Metadata-Version: 2.5
Name: rowstile
Version: 0.1.0-alpha.4
Summary: Access rules for Postgres: the rowstile command, and the SDK for Python apps (FastAPI, SQLAlchemy, psycopg, asyncpg, Alembic, pytest)
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Provides-Extra: asyncpg
Requires-Dist: asyncpg>=0.28; extra == 'asyncpg'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'fastapi'
Provides-Extra: psycopg
Requires-Dist: psycopg>=3.1; extra == 'psycopg'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# rowstile for Python

`pip install --pre rowstile` (`--pre` while only an alpha is published) installs the `rowstile` command (`rowstile init`, `dev`, `migrate`, `review`; also
`python -m rowstile`) and the SDK for Python apps. Extras bring the integrations' dependencies:
`rowstile[fastapi]`, `[sqlalchemy]`, `[psycopg]`, `[asyncpg]`. Python 3.11 or newer.

The SDK for Python apps on a rowstile policy: every transaction signs in as whoever the request or
the job acts for, refused writes become a 403 with the database's reason, rows the user can't see a 404,
and the app refuses to start on a connection that skips row-level security. It holds no rowstile logic: it
calls the `authz.*` functions the policy made, and translates their answers.

```python
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from rowstile.fastapi import Rowstile

engine = create_async_engine("postgresql+asyncpg://app@db/app")
app = FastAPI()
Rowstile(app, engine, user=current_user)     # current_user(request): its user's id, or None; before adding routes
```

| module | what |
|---|---|
| `rowstile` | `acting_as(who)`, `@job(who)`, `Principal`, `Refused`, `NotFound`, `refusal(exc)`, `error_code(exc)` (rowstile's code for any database error: `AZ709`), `act_as_sql()` |
| `rowstile.fastapi` | `Rowstile(app, engine, user=...)`: the request's principal, 403/404 problem bodies, the start-up check |
| `rowstile.sqlalchemy` | `install(engine)` (sync or async; SQLModel too), `ids(type, perm)` for `where`, `perms_of()` for a list's buttons, `can()` (`Queries[ObjectType, Permission]()` has the three taking only the generated client's names, so a misspelled one doesn't type-check), `expect()` for Core updates and deletes, `why_stale()` for the ORM's StaleDataError |
| `rowstile.psycopg` | `transaction(conn, who)`, `atransaction(aconn, who)`, `expect()`, `aexpect()` |
| `rowstile.asyncpg` | `transaction(conn, who)`, `expect()` |
| `rowstile.alembic` | `include_name`, `include_object`: autogenerate leaves rowstile's objects alone |
| `rowstile.testing` | pytest fixtures: `as_user`, `assert_refused`, `assert_not_found` (awaited, with an async function), `authz_owner_url`, `authz_app_url`; `database_per_worker(url)`: a copy of the migrated test database for each pytest-xdist worker |

Who a transaction acts for: `42` or `"42"` (a user), `("service", 3)` (another principal type of the policy),
or `None` (nobody: only what `anyone` may see; `None` given to a call is nobody too, whoever the code around
it acts for). A plain id is always a user's, whatever it holds: `"service:3"` is the user with that id, so an
id from outside (a username, an identity provider's subject) can't name a service. `Principal.parse("service:3")`
reads back what `str()` wrote, for text your own code wrote. Every transaction signs in, even when nobody is signed in, and nothing
uses a session-level `SET`, so pools and poolers in transaction mode are safe; behind one that keeps no prepared
statements, turn the driver's off ([Behind a pooler](../../docs/operations.md#behind-a-pooler)).

With FastAPI: `user(request)` runs in a middleware that `Rowstile(...)` adds, so give it the session or the
token to read from the request itself. What a middleware of yours puts on `request.state` is there only if
that middleware was added after `Rowstile(...)` (Starlette runs the one added last first); added before, it
hasn't run yet. `user` may raise an `HTTPException`. The start-up check runs whether or not the app has a `lifespan`. WebSockets
are not signed in: use `rowstile.acting_as()` in the endpoint. An engine in `AUTOCOMMIT` can't be signed in
(the sign-in lasts a transaction): its queries fail with `NotSignedIn`.

Policy changes ship as migrations for your tool (`rowstile migrate`, with `tool = "alembic"` in
`rowstile.toml`). The types and permissions of your policy are generated names (`rowstile client py`), so a
misspelled permission doesn't type-check.

`integrations/fastapi/` is the conformance suite: a small FastAPI app on SQLAlchemy (async, asyncpg) and
Alembic, and the checks every supported stack passes (`integrations/fastapi/test.sh`).
