Metadata-Version: 2.4
Name: saasbase
Version: 0.1.0
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 — Python SDK

Embedded Python SDK for building multi-tenant SaaS products with first-class support for users, services, groups, and AI agents.

Mirrors the Java SDK 1:1 in behavior and database schema, but with Pythonic snake_case naming and a familiar fluent builder.

## Install

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

## Quick start

```python
from saasbase import SaaSBase

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

with sb.as_user("alice") as ctx:
    org = ctx.organizations.create("acme")
    project = ctx.projects.create(org.id, "alpha")
    doc = ctx.documents.upload(
        project.id,
        "report.pdf",
        content=open("report.pdf", "rb"),
        media_type="application/pdf",
    )

sb.close()
```

## FastAPI integration (optional)

The `saasbase.fastapi` subpackage exposes every domain API over async HTTP, mirroring the Java
Spring Boot starter. Handlers are `async def` and offload blocking SaaSBase calls onto a thread
pool via `anyio.to_thread.run_sync`, so the event loop stays non-blocking under concurrent load.

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

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

# uvicorn my_module:app --reload
```

Or let the module build a `SaaSBase` from env vars (prefix `SAASBASE_`, double-underscore for
nested keys) — matches the Java starter's YAML property names:

```bash
export SAASBASE_DB_URL=jdbc:sqlite:./data/saasbase.db
export SAASBASE_STORAGE__LOCAL__BASE_PATH=./data/documents
export SAASBASE_WEB__BASE_PATH=/api
python -c "from saasbase.fastapi import create_app; app = create_app()"
```

Every request is attributed to an actor via the `X-SaaSBase-Actor` header, parsed as `type:id`:

```bash
curl -H "X-SaaSBase-Actor: user:alice" \
     -H "Content-Type: application/json" \
     -d '{"slug":"acme","name":"Acme Corp"}' \
     http://localhost:8000/api/organizations
```

`create_app(actor_resolver=...)` accepts a custom async resolver (e.g. a JWT-aware one). When a
resolver returns `None`, the request falls back to `saasbase.actor.anonymous` (default
`service:anonymous`) — set it to empty to force 401 on unauthenticated requests.

Endpoints under `/api` (configurable): `/organizations`, `/projects`, `/documents` (incl.
multipart upload + stream download + versioning), `/api-calls`, `/executions`, `/memberships`,
`/audit-events`, `/authz/check|explain|effective-roles|who-can|who-can-explain`. Interactive
OpenAPI docs at `/docs`.

## 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 (e.g. an `ORG_OWNER` key can act on the org's
projects).

```python
from saasbase import CreateApiKeyRequest, Role, SaaSBase

sb = SaaSBase.builder().sqlite().local_storage("./data").build()
with sb.as_user("alice") as ctx:
    created = ctx.api_keys.create(
        CreateApiKeyRequest(scope=project.ref, role=Role.PROJECT_EDITOR, name="ci")
    )
    print(created.token)  # sbk_<prefix>.<secret> — only visible once
    print(created.api_key.id, created.api_key.is_active)
```

Over HTTP (FastAPI), authenticate by passing the token as `Authorization: Bearer sbk_...`:

```bash
curl -H "Authorization: Bearer sbk_abcdef123456.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
     http://127.0.0.1:8000/api/projects/$PROJECT_ID
```

Revocation is soft (`POST /api/api-keys/{id}/revoke`) — the key is kept for audit purposes and
immediately fails authentication. `expires_at`, `last_used_at`, and arbitrary `metadata` are also
supported.

## Database schema

The SQL migration files in [src/saasbase/sqlite/migrations/](src/saasbase/sqlite/migrations/) and
[src/saasbase/postgres/migrations/](src/saasbase/postgres/migrations/) are kept byte-identical to the Java SDK's
migrations so a single database can be used by both SDKs.

## Module layout

```
src/saasbase/
├── common/            # shared enums, records, pagination, queries, exceptions
├── api/               # public API Protocols + request/response DTOs
├── spi/               # SPI record types + Protocols for backends
├── persistence/       # DB-API-backed repository implementations
├── sqlite/            # SQLite connection factory + migrations
├── postgres/          # Postgres connection factory + migrations
├── storage/           # local + S3 document storage adapters
├── authz_inmemory/    # in-memory authorization engine
├── authz_openfga/     # OpenFGA authorization engine
├── audit_inmemory/    # in-memory audit sink
├── core/              # default runtime (builder, SaaSBase/Context, API impls)
└── fastapi/           # optional async FastAPI app + routers
```

## Examples

See [examples/](examples/):

- `example_app.py` — end-to-end demo (runs once and exits)
- `document_centric.py` — upload + version documents
- `apicall_centric.py` — API call tracking with correlation IDs
- `agent_assisted.py` — agent execution lifecycle
- `document_versioning.py` — named document versions
- `fastapi_server.py` — run the async REST API with uvicorn:

  ```bash
  pip install 'saasbase[fastapi]' uvicorn
  python examples/fastapi_server.py       # serves http://127.0.0.1:8000/docs
  # or with auto-reload:
  uvicorn examples.fastapi_server:app --reload

  # in another shell:
  curl -H 'X-SaaSBase-Actor: user:alice' \
       -H 'Content-Type: application/json' \
       -d '{"slug":"acme","name":"Acme Corp"}' \
       http://127.0.0.1:8000/api/organizations
  ```

## Publishing to PyPI

Releases are published by the GitHub Actions workflow [.github/workflows/publish-python.yml](../.github/workflows/publish-python.yml).

The workflow authenticates via **PyPI Trusted Publishers (OIDC)** — no API tokens are stored in
the repo. A one-time setup is required on PyPI before the first run:

1. **Create the project on PyPI** (or TestPyPI) by uploading the first version manually with
   `twine`, or register the name as a Pending Publisher:
   <https://pypi.org/manage/account/publishing/>.

2. **Add a Trusted Publisher** (PyPI → project → Publishing → Add a new pending / trusted
   publisher) with these values:

   | Field | Value |
   | --- | --- |
   | Owner | your GitHub org/user (e.g. `sireto`) |
   | Repository | `saasbase` |
   | Workflow filename | `publish-python.yml` |
   | Environment | `pypi` (and `testpypi` for TestPyPI) |

3. **Create matching GitHub environments** under the repo's Settings → Environments: `pypi` and
   `testpypi`. Add reviewers on the `pypi` environment if you want a manual approval gate.

### Cutting a release

The version is derived from the git tag — there is **nothing to bump in `pyproject.toml`**.
[hatch-vcs](https://github.com/ofek/hatch-vcs) reads the nearest tag and stamps the built wheel
with it, also writing `src/saasbase/_version.py` so `saasbase.__version__` returns the same value
at runtime.

1. Push a GitHub Release whose tag is the version you're cutting. Both `v0.2.0` and `0.2.0` are
   accepted and both produce PyPI version `0.2.0`.
2. The workflow: checks out with full history, resolves the version via `hatch version`, verifies
   the tag matches it (catches stray commits between tag and HEAD), runs tests, builds sdist +
   wheel, and publishes to PyPI with Sigstore attestations attached.

Between releases, local builds resolve to a dev version such as `0.2.0.dev3+g<sha>` based on the
commit distance from the last tag.

### Dry-runs against TestPyPI

Run the workflow manually from the Actions tab → **Publish Python package** → *Run workflow* and
pick `testpypi` as the target.
