Metadata-Version: 2.4
Name: tinysdk
Version: 0.9.3
Summary: Shared services for TinyAI microservices platform
Author: Synapze GmbH
Maintainer: Synapze GmbH
License: MIT
Project-URL: Homepage, https://github.com/SynapzeGmbH/TinySDK
Project-URL: Repository, https://github.com/SynapzeGmbH/TinySDK
Project-URL: Issues, https://github.com/SynapzeGmbH/TinySDK/issues
Keywords: microservices,mongodb,minio,temporal,tinyai
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pymongo>=4.6.3
Requires-Dist: minio>=7.0.0
Requires-Dist: meilisearch>=0.31.0
Requires-Dist: kubernetes>=28.1.0
Requires-Dist: urllib3
Requires-Dist: certifi
Requires-Dist: temporalio>=1.7.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=4.0.0; extra == "dev"
Requires-Dist: mongomock>=4.1.2; extra == "dev"
Dynamic: license-file

# TinySDK

Shared service clients for the TinyAI microservices platform — MongoDB, MinIO, Temporal workers, and structured logging with a consistent interface.

## Installation

```bash
pip install tinysdk
```

## Quick Start

```python
from tinysdk import TinyDBService, TinyStorageService, TinyWorkerService, TinyWorkflowService, get_temporal_client, TinyLogger

# MongoDB
db = TinyDBService()
doc_id = db.create("users", {"name": "John", "email": "john@example.com"})
users = db.read("users", filter={"name": "John"})
count = db.update("users", {"name": "John"}, {"$set": {"active": True}})
count = db.delete("users", {"name": "John"})
exists = db.exists("users", {"email": "john@example.com"})
ids = db.create_multiple("users", [{"name": "Alice"}, {"name": "Bob"}])
db.close()

# MinIO / S3
storage = TinyStorageService()
storage.upload_file("my-bucket", "file.txt", buffer)  # file_size auto-detected
storage.upload_file("my-bucket", "file.txt", buffer, 1024)  # or pass explicitly
file_buffer, name = storage.download_file("my-bucket", "file.txt")
storage.upload_folder("my-bucket", "/local/folder")
storage.download_folder("my-bucket", "folder-name", "/local/dest")
storage.close()

# Temporal (activity/workflow worker for a task queue)
from temporalio import activity


@activity.defn
async def my_activity(payload: dict) -> None: ...


worker = TinyWorkerService()  # logger optional; defaults to TinyLogger()
await worker.start(task_queue="my-queue", activities=[my_activity])  # or workflows=[...]

# Or just a connected client
client = await get_temporal_client()

# Temporal (read-only workflow queries) — async, call from an async function
workflow_svc = TinyWorkflowService()
status = await workflow_svc.get_status("order-123")  # status, timings, task queue
history = await workflow_svc.get_history_summary("order-123")  # condensed event history
summary = await workflow_svc.get_summary("order-123")  # status + history in one call

# Logger
logger = TinyLogger()
logger.log("something happened", level="INFO", context={"key": "value"})
logger.start_heartbeat()  # emits periodic HEARTBEAT events
logger.stop_heartbeat()  # clean shutdown

doc_logger = logger.for_document("doc-id-123")
doc_logger.log("processing started")
```

## Configuration

All clients read credentials from environment variables.

| Service | Variable | Example |
|---------|----------|---------|
| MongoDB | `DB_LINK` | `mongodb://mongo:27017` |
| MongoDB | `DB_NAME` | `tinyDatabase` |
| MinIO | `MINIO_ENDPOINT` | `minio:9000` |
| MinIO | `MINIO_ACCESSKEY` | `minioadmin` |
| MinIO | `MINIO_SECRETKEY` | `minioadmin` |
| Temporal | `TEMPORAL_ADDRESS` | `temporal-server:7233` |
| Temporal | `TEMPORAL_NAMESPACE` | `default` |
| Logger | `SERVICE_NAME` | `my-service` |
| Logger | `CLUSTER` | `prod` |
| Logger | `NAMESPACE` | `default` |
| Logger | `LOG_LEVEL` | `INFO` |
| Logger | `HEARTBEAT_INTERVAL` | `30` |

Credentials can also be passed directly as constructor arguments.

## Storage backends

`TinyStorageService` addresses storage by **logical namespace** (`customer-documents`,
`tiny-models`). A router resolves each namespace to a target and a physical location.

With no `TINYSTORAGE_*` variables set, every namespace resolves to the `platform`
target — S3, bucket name equal to the namespace. This is the default and needs no
configuration.

To route some namespaces to client-owned storage:

```bash
TINYSTORAGE_CLIENT_BACKEND=<registered backend name>
TINYSTORAGE_CLIENT_ROOT=/mnt/client-storage
TINYSTORAGE_MAP=customer-documents=client,finalized-documents=client
```

Both halves of that configuration are validated at startup, because either mistake
ends with customer data on Synapze S3 and nothing to notice:

- mapping a namespace to `client` with no client backend configured, and
- configuring a client backend that no namespace is mapped to,

are both startup errors, never a silent fallback. So is mapping the same namespace
twice. Namespaces must match `[a-z0-9]([a-z0-9.-]*[a-z0-9])?` — S3 bucket syntax —
because a namespace becomes a path segment under the client root, and anything
outside that charset can never match a real call site and would only ever be a typo
routed silently to platform S3.

Every reachable backend is constructed at startup, so a malformed endpoint or an
option the adapter does not accept fails at boot. **This is not a reachability
check**: no client library performs network I/O in its constructor, so an
unreachable host or a wrong secret still surfaces on first use.

`TinyStorageService.client` exposes the raw Minio client for the platform target and
ignores routing entirely. It therefore **raises** as soon as any namespace is mapped
to `client` — otherwise a caller could read or write customer-owned data straight
from Synapze S3. Use `list_objects()` and the routed methods instead.


### Adding a backend

1. Add a module under `src/tinysdk/storage/backends/` implementing `StorageBackend`.
2. Add it to `REGISTRY` in `backends/__init__.py`.
3. Add a test subclassing the shipped conformance suite:

```python
from tinysdk.storage.conformance import StorageBackendConformance


class TestMyBackendConformance(StorageBackendConformance):
  def make_backend(self):
    return MyBackend(...)
```

Path-based backends must:

- call `validate_key` on every key, and set `enforces_key_policy = True` on their
  conformance subclass so the suite checks it;
- **also** confirm the resolved path is still under their root
  (`os.path.realpath(p).startswith(root)`) — `validate_key` vets the key string, but
  a symlink already inside the root can redirect an otherwise valid key;
- write via a temporary file **in the target directory** followed by an atomic
  rename — a temp file elsewhere makes the rename cross-device, which silently
  degrades to a copy and loses atomicity;
- decide how to handle unicode normalization (NFC and NFD spellings collide on APFS
  and SharePoint) and case-insensitive volumes (`A.txt` vs `a.txt`).

Options reach an adapter from `TINYSTORAGE_CLIENT_*` as **strings**; an adapter with
non-string options must coerce them itself.

## License

MIT
