Metadata-Version: 2.4
Name: saasbase
Version: 0.1.1
Summary: Embedded Python SDK for building multi-tenant SaaS products with users, services, groups, and AI agents.
Project-URL: Homepage, https://github.com/sireto/saasbase
Project-URL: Repository, https://github.com/sireto/saasbase
Author: Sireto
License: Apache-2.0
Keywords: agents,audit,authorization,multi-tenant,saas
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Provides-Extra: all
Requires-Dist: anyio>=4.0; extra == 'all'
Requires-Dist: boto3>=1.28; extra == 'all'
Requires-Dist: fastapi>=0.110; extra == 'all'
Requires-Dist: httpx>=0.25; extra == 'all'
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
Requires-Dist: pydantic-settings>=2.0; extra == 'all'
Requires-Dist: pydantic>=2.5; extra == 'all'
Requires-Dist: python-multipart>=0.0.9; extra == 'all'
Provides-Extra: dev
Requires-Dist: anyio>=4.0; extra == 'dev'
Requires-Dist: boto3>=1.28; extra == 'dev'
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx>=0.25; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
Requires-Dist: pydantic-settings>=2.0; extra == 'dev'
Requires-Dist: pydantic>=2.5; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: python-multipart>=0.0.9; extra == 'dev'
Requires-Dist: ruff>=0.3; extra == 'dev'
Requires-Dist: uvicorn>=0.27; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: anyio>=4.0; extra == 'fastapi'
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
Requires-Dist: pydantic-settings>=2.0; extra == 'fastapi'
Requires-Dist: pydantic>=2.5; extra == 'fastapi'
Requires-Dist: python-multipart>=0.0.9; extra == 'fastapi'
Provides-Extra: openfga
Requires-Dist: httpx>=0.25; extra == 'openfga'
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
Provides-Extra: s3
Requires-Dist: boto3>=1.28; extra == 's3'
Description-Content-Type: text/markdown

# SaaSBase

[![PyPI](https://img.shields.io/pypi/v/saasbase.svg)](https://pypi.org/project/saasbase/)
[![Python versions](https://img.shields.io/pypi/pyversions/saasbase.svg)](https://pypi.org/project/saasbase/)
[![License](https://img.shields.io/pypi/l/saasbase.svg)](https://github.com/sireto/saasbase)

**Embedded SDK for building multi-tenant SaaS products.** First-class support for users, services,
groups, and AI agents — all treated as actors with per-resource role-based authorization, durable
audit trails, and execution tracing for agentic workflows.

Modelled around a simple resource hierarchy: `Organization → Project → { Document | ApiCall }`.
Pluggable backends for storage, database, and authorization. No Django/Flask/Spring baggage —
just a Python API you wire into whatever HTTP layer you already use (an optional async FastAPI
adapter ships in the box).

Ships alongside a [Java SDK](https://github.com/sireto/saasbase/tree/main/java) with a
byte-identical database schema, so a single database can be driven by either SDK.

## Highlights

- **Actors as first-class citizens** — `USER`, `SERVICE`, `AGENT`, `GROUP`, `API_KEY`. Every
  operation is attributed to one.
- **Role-based authorization with scope inheritance** — grants on an organization automatically
  flow to its projects, documents, and API calls.
- **Named document versioning** — stable document IDs; named versions (`"draft"`, `"v2"`,
  `"final"`) each with their own content and metadata.
- **Execution tracing** — start/complete/fail flows for agent and service operations, with
  correlation IDs, parent/child relationships, and model provenance.
- **API keys** — scoped to an organization or project, carrying a single role.
  `sbk_<prefix>.<secret>` tokens, soft revocation, optional expiry, `last_used_at` tracking.
- **Durable audit log** — every mutation flows through a configurable sink with explicit
  retention policies.
- **Pluggable backends** — SQLite or PostgreSQL, local filesystem or S3, in-memory or OpenFGA
  authorization.
- **Optional async FastAPI surface** — `create_app(sb)` exposes every domain API over HTTP with
  automatic Bearer-token authentication for API keys and OpenAPI docs at `/docs`.

## Install

```bash
pip install saasbase                   # core (SQLite + local storage + in-memory authz)
pip install "saasbase[postgres]"       # + PostgreSQL
pip install "saasbase[s3]"             # + S3-compatible storage
pip install "saasbase[openfga]"        # + OpenFGA authorization
pip install "saasbase[fastapi]"        # + async FastAPI REST surface
pip install "saasbase[all]"            # everything
```

Python 3.11 or newer.

## Quick start

```python
from saasbase import SaaSBase

sb = (
    SaaSBase.builder()
    .sqlite()                           # or .postgres(), or .db_url("...")
    .local_storage("./data")            # or .s3_storage(...)
    .build()
)

with sb.as_user("alice") as ctx:
    org = ctx.organizations.create("acme", name="Acme Corp")
    project = ctx.projects.create(org.id, "platform")

    with open("invoice.pdf", "rb") as f:
        doc = ctx.documents.upload(
            project.id,
            name="invoice.pdf",
            content=f,
            media_type="application/pdf",
        )

    print(doc.current_version_name)     # "v1"
    print(doc.checksum)                 # sha-256 hex

sb.close()
```

Change the actor to change who audit events are attributed to:

```python
with sb.as_service("billing") as ctx: ...
with sb.as_agent("ocr-v1") as ctx: ...
```

## API keys

Each key binds to one resource (`ORGANIZATION` or `PROJECT`) and carries a single `Role`. When a
key authenticates, it acts as an `API_KEY` actor whose permissions are exactly the role's actions
on the scoped resource — and inherited scopes. An `ORG_OWNER` key, for example, can act on any
project, document, or API call inside its organization.

```python
from saasbase import CreateApiKeyRequest, Role

with sb.as_user("alice") as ctx:
    created = ctx.api_keys.create(
        CreateApiKeyRequest(scope=project.ref, role=Role.PROJECT_EDITOR, name="ci")
    )
    # The raw `token` is visible exactly once — store it in a secret manager.
    print(created.token)                # sbk_<prefix>.<secret>
    print(created.api_key.id)

    # Authenticate a token back to an active API key:
    resolved = ctx.api_keys.authenticate(created.token)
    assert resolved is not None and resolved.is_active

    # Soft-revoke — record is kept for audit; future auth fails.
    ctx.api_keys.revoke(created.api_key.id)
```

`expires_at`, `last_used_at` (touched on each successful authentication), and free-form
`metadata` are all supported.

## FastAPI integration (optional)

`saasbase.fastapi` exposes every domain API over async HTTP. Handlers are `async def` and offload
blocking SaaSBase calls to a thread pool via `anyio.to_thread.run_sync`, so concurrent requests
don't stall the event loop.

```python
from saasbase import SaaSBase
from saasbase.fastapi import create_app

sb = SaaSBase.builder().sqlite().local_storage("./data").build()
app = create_app(sb)

# uvicorn my_module:app --reload
```

Or let the module build a `SaaSBase` from env vars — useful in containerized deployments:

```bash
export SAASBASE_DB_URL=jdbc:sqlite:./data/saasbase.db
export SAASBASE_STORAGE__LOCAL__BASE_PATH=./data/documents
export SAASBASE_WEB__BASE_PATH=/api
uvicorn my_app:app
```

Every request is attributed to an actor, resolved in this order:

1. `Authorization: Bearer sbk_<prefix>.<secret>` — SaaSBase API key; request acts as the
   `API_KEY` actor.
2. `X-SaaSBase-Actor: <type>:<id>` header. Values: `user:alice`, `service:billing`,
   `agent:tagger-v1`, `group:admins`, `api_key:<uuid>`.
3. The configured anonymous fallback (default `service:anonymous`). Set it empty to force 401.

```bash
# Authenticate as an API key
curl -H "Authorization: Bearer sbk_abcdef123456.xxxxxxx" \
     https://api.example.com/api/projects/$PROJECT_ID

# Or as an impersonated user
curl -H "X-SaaSBase-Actor: user:alice" \
     -H "Content-Type: application/json" \
     -d '{"slug":"acme"}' \
     https://api.example.com/api/organizations
```

`create_app(actor_resolver=...)` accepts a custom async resolver — e.g. one that decodes a JWT.

Endpoints under the configurable base path (default `/api`):

- `/organizations`, `/projects`, `/documents` (multipart upload, streamed download, versioning)
- `/api-calls`, `/executions` (start/complete/fail + correlation listing + children)
- `/memberships`, `/api-keys`
- `/audit-events` (query, by-resource, by-correlation, prune)
- `/authz/{check,explain,effective-roles,who-can,who-can-explain}`

Interactive OpenAPI docs at `/docs`.

## Examples

Runnable scripts live in the
[examples/](https://github.com/sireto/saasbase/tree/main/python/examples) directory in the
repository:

- `example_app.py` — end-to-end CLI demo
- `document_centric.py` — upload + list + filter documents
- `apicall_centric.py` — track API calls with correlation IDs
- `agent_assisted.py` — agent execution lifecycle with provenance
- `document_versioning.py` — named document versions
- `fastapi_server.py` — run the async REST API with uvicorn

## Authorization model

Roles carry a fixed action set (see `saasbase.AuthorizationPolicy`):

| Role                  | Typical grant                                                |
| --------------------- | ------------------------------------------------------------ |
| `ORG_OWNER`           | Full control on an org and everything beneath it             |
| `ORG_ADMIN`           | Manage projects and members; cannot delete the org            |
| `ORG_MEMBER`          | Read-only at the org level                                    |
| `PROJECT_OWNER`       | Full control on one project and its resources                 |
| `PROJECT_EDITOR`      | Create/update documents and API calls                         |
| `PROJECT_VIEWER`      | Read-only within a project                                    |
| `DOCUMENT_*`          | Resource-level grants on a single document                    |
| `API_CALL_*`          | Resource-level grants on a single API call                    |

Grants are evaluated against the resource hierarchy, so a role granted at a higher scope applies
to descendants. Use `authorization.explain(actor, action, resource)` to see which role in which
scope allowed (or denied) a call.

## Links

- 📦 **PyPI**: <https://pypi.org/project/saasbase/>
- 🐙 **Source code**: <https://github.com/sireto/saasbase>
- ☕ **Java SDK** (same database schema): <https://github.com/sireto/saasbase/tree/main/java>
- 💬 **Issues**: <https://github.com/sireto/saasbase/issues>

## License

Licensed under the Apache License, Version 2.0. See
[LICENSE](https://github.com/sireto/saasbase/blob/main/LICENSE) in the source repository.
