torii-backend 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. torii_backend-0.0.2/.github/workflows/ci.yml +65 -0
  2. torii_backend-0.0.2/.github/workflows/release.yml +70 -0
  3. torii_backend-0.0.2/.gitignore +13 -0
  4. torii_backend-0.0.2/CONTRIBUTING.md +57 -0
  5. torii_backend-0.0.2/LICENSE +21 -0
  6. torii_backend-0.0.2/PKG-INFO +143 -0
  7. torii_backend-0.0.2/README.md +107 -0
  8. torii_backend-0.0.2/openapitools.json +7 -0
  9. torii_backend-0.0.2/pyproject.toml +81 -0
  10. torii_backend-0.0.2/spec/server-v1.json +1 -0
  11. torii_backend-0.0.2/src/torii_backend/__init__.py +57 -0
  12. torii_backend-0.0.2/src/torii_backend/client.py +307 -0
  13. torii_backend-0.0.2/src/torii_backend/errors.py +42 -0
  14. torii_backend-0.0.2/src/torii_backend/fastapi.py +58 -0
  15. torii_backend-0.0.2/src/torii_backend/generated/__init__.py +64 -0
  16. torii_backend-0.0.2/src/torii_backend/generated/api/__init__.py +6 -0
  17. torii_backend-0.0.2/src/torii_backend/generated/api/server_sessions_api.py +853 -0
  18. torii_backend-0.0.2/src/torii_backend/generated/api/server_users_api.py +2017 -0
  19. torii_backend-0.0.2/src/torii_backend/generated/api_client.py +804 -0
  20. torii_backend-0.0.2/src/torii_backend/generated/api_response.py +21 -0
  21. torii_backend-0.0.2/src/torii_backend/generated/configuration.py +596 -0
  22. torii_backend-0.0.2/src/torii_backend/generated/exceptions.py +218 -0
  23. torii_backend-0.0.2/src/torii_backend/generated/models/__init__.py +23 -0
  24. torii_backend-0.0.2/src/torii_backend/generated/models/create_user_request.py +129 -0
  25. torii_backend-0.0.2/src/torii_backend/generated/models/cursor_page_response_user_response.py +106 -0
  26. torii_backend-0.0.2/src/torii_backend/generated/models/problem_detail.py +98 -0
  27. torii_backend-0.0.2/src/torii_backend/generated/models/server_user_search_request.py +128 -0
  28. torii_backend-0.0.2/src/torii_backend/generated/models/update_user_request.py +132 -0
  29. torii_backend-0.0.2/src/torii_backend/generated/models/user_response.py +164 -0
  30. torii_backend-0.0.2/src/torii_backend/generated/models/user_session_response.py +114 -0
  31. torii_backend-0.0.2/src/torii_backend/generated/py.typed +0 -0
  32. torii_backend-0.0.2/src/torii_backend/generated/rest.py +263 -0
  33. torii_backend-0.0.2/src/torii_backend/py.typed +0 -0
  34. torii_backend-0.0.2/src/torii_backend/types.py +44 -0
  35. torii_backend-0.0.2/src/torii_backend/verify.py +156 -0
  36. torii_backend-0.0.2/tests/test_patch_semantics.py +153 -0
  37. torii_backend-0.0.2/tests/test_verify.py +175 -0
@@ -0,0 +1,65 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ # Match the python versions listed in pyproject.toml classifiers.
23
+ python: ['3.9', '3.10', '3.11', '3.12']
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Set up Python ${{ matrix.python }}
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python }}
31
+
32
+ - name: Install uv (matches CONTRIBUTING.md dev flow)
33
+ uses: astral-sh/setup-uv@v3
34
+
35
+ - name: Install dependencies
36
+ run: |
37
+ uv venv
38
+ uv pip install -e ".[dev]"
39
+
40
+ - name: Ruff (lint)
41
+ run: .venv/bin/ruff check src/torii_backend tests
42
+
43
+ - name: Pytest
44
+ run: .venv/bin/pytest -q
45
+
46
+ build:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+
51
+ - name: Set up Python
52
+ uses: actions/setup-python@v5
53
+ with:
54
+ python-version: '3.12'
55
+
56
+ - name: Build sdist + wheel
57
+ run: |
58
+ python -m pip install --upgrade build
59
+ python -m build
60
+
61
+ - name: Verify wheel installs cleanly
62
+ run: |
63
+ python -m venv /tmp/verify-venv
64
+ /tmp/verify-venv/bin/pip install dist/*.whl
65
+ /tmp/verify-venv/bin/python -c "import torii_backend; print(torii_backend.__name__)"
@@ -0,0 +1,70 @@
1
+ name: Release
2
+
3
+ # Tag-triggered PyPI publish via OIDC trusted publishing.
4
+ #
5
+ # Prerequisites (one-time, see torii repo docs/Publishing-SDKs.md):
6
+ # 1. pypi.org → "Your projects" → "Publishing" → Add pending publisher
7
+ # (or, after first publish, regular trusted publisher) with:
8
+ # - PyPI project name: torii-backend
9
+ # - Owner: Torii-ApS
10
+ # - Repository: torii-sdk-python
11
+ # - Workflow: release.yml
12
+ # - Environment: pypi
13
+ # 2. GitHub repo → Settings → Environments → create `pypi` with required
14
+ # reviewers for manual approval before publish.
15
+
16
+ on:
17
+ push:
18
+ tags: ['v*']
19
+
20
+ permissions:
21
+ contents: write # for GH release creation
22
+ id-token: write # required for OIDC trusted publishing
23
+
24
+ jobs:
25
+ release:
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Set up Python
32
+ uses: actions/setup-python@v5
33
+ with:
34
+ python-version: '3.12'
35
+
36
+ - name: Assert tag matches pyproject.toml version
37
+ run: |
38
+ PKG_VERSION=$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
39
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
40
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
41
+ echo "Tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${PKG_VERSION}" >&2
42
+ exit 1
43
+ fi
44
+
45
+ - name: Install uv
46
+ uses: astral-sh/setup-uv@v3
47
+
48
+ - name: Test on tagged SHA
49
+ run: |
50
+ uv venv
51
+ uv pip install -e ".[dev]"
52
+ .venv/bin/pytest -q
53
+
54
+ - name: Build sdist + wheel
55
+ run: |
56
+ python -m pip install --upgrade build
57
+ python -m build
58
+
59
+ - name: Publish to PyPI
60
+ uses: pypa/gh-action-pypi-publish@release/v1
61
+ # No `with: password` — OIDC trusted publisher exchanges the
62
+ # GitHub-issued ID token for a short-lived PyPI upload token.
63
+
64
+ - name: Create GitHub Release
65
+ env:
66
+ GH_TOKEN: ${{ github.token }}
67
+ run: |
68
+ gh release create "${GITHUB_REF_NAME}" \
69
+ --title "${GITHUB_REF_NAME}" \
70
+ --generate-notes
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ *.egg-info/
9
+ build/
10
+ dist/
11
+ .DS_Store
12
+ .idea/
13
+ .vscode/
@@ -0,0 +1,57 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in `torii-backend`!
4
+
5
+ ## Reporting bugs
6
+
7
+ Open an issue with:
8
+
9
+ - The version of `torii-backend` you're using (`pip show torii-backend`).
10
+ - A minimal reproduction — a few lines that exhibit the bug.
11
+ - What you expected to happen vs. what actually happened.
12
+
13
+ For security-sensitive issues (anything that could let an attacker forge or bypass token verification), please email **security@torii.so** instead of filing a public issue.
14
+
15
+ ## Development
16
+
17
+ ```sh
18
+ git clone https://github.com/Torii-ApS/torii-sdk-python
19
+ cd torii-sdk-python
20
+ uv venv
21
+ uv pip install -e ".[dev]"
22
+ .venv/bin/pytest -q
23
+ ```
24
+
25
+ The REST client under `src/torii_backend/generated/` is produced by [`openapi-generator`](https://openapi-generator.tech/) from `spec/server-v1.json`. Don't hand-edit it. To regenerate after a spec update:
26
+
27
+ ```sh
28
+ bunx -y @openapitools/openapi-generator-cli generate \
29
+ -i spec/server-v1.json -g python -o /tmp/python-gen-raw \
30
+ --additional-properties=packageName=torii_backend.generated,projectName=torii-backend-generated,library=urllib3
31
+ cp -r /tmp/python-gen-raw/torii_backend/generated src/torii_backend/generated
32
+ ```
33
+
34
+ The hand-written surface (`client.py`, `verify.py`, `fastapi.py`, `types.py`, `errors.py`) is where bug reports and PRs typically land.
35
+
36
+ ## Pull requests
37
+
38
+ 1. Open an issue first for non-trivial changes so we can discuss the shape.
39
+ 2. Branch off `main`, name it `fix/<short>` or `feat/<short>`.
40
+ 3. Run `.venv/bin/ruff check src/torii_backend tests` and `.venv/bin/pytest -q` before pushing — CI checks both across Python 3.9–3.12.
41
+ 4. Keep PRs small and focused. One concern per PR.
42
+ 5. Update `README.md` if you change the public surface.
43
+
44
+ ## Releases
45
+
46
+ Tagged off `main`. Bump `version` in `pyproject.toml` and any references in `README.md`, then:
47
+
48
+ ```sh
49
+ git tag v0.0.2
50
+ git push origin v0.0.2
51
+ ```
52
+
53
+ Publishing to PyPI is handled by a maintainer; ping in the release PR if you need a cut.
54
+
55
+ ## Code of Conduct
56
+
57
+ Be kind. Disagreements happen; argue the position, not the person.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GOOD Code ApS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: torii-backend
3
+ Version: 0.0.2
4
+ Summary: Backend SDK for torii — verify JWTs, manage users, react to events from your Python server.
5
+ Project-URL: Homepage, https://torii.so
6
+ Project-URL: Source, https://github.com/Torii-ApS/torii-sdk-python
7
+ Project-URL: Issues, https://github.com/Torii-ApS/torii-sdk-python/issues
8
+ Author: GOOD Code ApS
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: auth,backend,jwt,oidc,torii
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: pydantic>=2.11
23
+ Requires-Dist: pyjwt[crypto]<3.0,>=2.8
24
+ Requires-Dist: python-dateutil>=2.8.2
25
+ Requires-Dist: typing-extensions>=4.7.1
26
+ Requires-Dist: urllib3<3.0.0,>=2.1.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Provides-Extra: fastapi
34
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # torii-backend
38
+
39
+ Backend SDK for [torii](https://torii.so) — verify end-user JWTs without a per-request round trip and manage users from your Python server.
40
+
41
+ > **v0.x — API may still change.**
42
+
43
+ ## Setup
44
+
45
+ 1. Sign in to [app.torii.so](https://app.torii.so) and from your dashboard copy:
46
+ - your **issuer URL** (e.g. `https://acme.torii.so`)
47
+ - a **secret key** (`sk_test_…` for development, `sk_live_…` for production)
48
+
49
+ 2. Install the SDK:
50
+
51
+ ```sh
52
+ pip install torii-backend
53
+ # or, with the FastAPI dependency adapter:
54
+ pip install "torii-backend[fastapi]"
55
+ ```
56
+
57
+ Python 3.9+.
58
+
59
+ 3. Verify an end-user JWT:
60
+
61
+ ```python
62
+ from torii_backend import verify_token
63
+
64
+ auth = verify_token(token, issuer="https://acme.torii.so")
65
+ print(auth.user_id, auth.environment_id, auth.email_verified)
66
+ ```
67
+
68
+ The first call fetches the issuer's JWKS; subsequent calls reuse the cache and rotate keys automatically (handled by [`PyJWT`](https://pyjwt.readthedocs.io/)). No round trip per request.
69
+
70
+ 4. Call the backend REST API:
71
+
72
+ ```python
73
+ import os
74
+ from torii_backend import create_torii_client
75
+
76
+ torii = create_torii_client(secret_key=os.environ["TORII_SECRET_KEY"])
77
+ user = torii.users.get(user_id)
78
+ ```
79
+
80
+ Default base URL is `https://api.torii.so`. Override with `api_url=` for staging or self-hosted.
81
+
82
+ ## FastAPI
83
+
84
+ ```python
85
+ from fastapi import Depends, FastAPI
86
+ from torii_backend.fastapi import require_auth
87
+
88
+ app = FastAPI()
89
+ auth_dep = require_auth(issuer="https://acme.torii.so")
90
+
91
+ @app.get("/me")
92
+ def me(auth = Depends(auth_dep)):
93
+ return {"user_id": auth.user_id}
94
+ ```
95
+
96
+ ## Backend API
97
+
98
+ ```python
99
+ page = torii.users.list(limit=50)
100
+ user = torii.users.create(email="x@y.com")
101
+ torii.users.ban(user.id)
102
+
103
+ sessions = torii.sessions.list_for_user(user.id)
104
+ torii.sessions.revoke_all_for_user(user.id)
105
+ ```
106
+
107
+ ### PATCH semantics (`users.update`)
108
+
109
+ `users.update` is a tri-state PATCH: each updatable field can be **set**, **cleared**, or **left alone**. Pass only the kwargs you want on the wire — pydantic v2's `model_fields_set` tracks which fields were explicitly provided, and the SDK serializes via `model_dump(exclude_unset=True)` so untouched fields never appear in the request body.
110
+
111
+ ```python
112
+ torii.users.update(
113
+ user_id,
114
+ name="Ada", # → server updates name
115
+ phone=None, # → server clears phone (sent as JSON null)
116
+ # address not passed → server leaves alone
117
+ )
118
+ ```
119
+
120
+ Wire body for the call above:
121
+
122
+ ```json
123
+ { "name": "Ada", "phone": null }
124
+ ```
125
+
126
+ | Call | Field on wire | Server effect |
127
+ | ------------------------------------- | ----------------- | ------------------ |
128
+ | `users.update(id, name="Ada")` | `"name": "Ada"` | update `name` |
129
+ | `users.update(id, phone=None)` | `"phone": null` | clear `phone` |
130
+ | `users.update(id)` (no field kwargs) | omitted | leave alone |
131
+
132
+ You can also build a request explicitly — useful when assembling the patch dynamically:
133
+
134
+ ```python
135
+ from torii_backend import ToriiUpdateUserInput
136
+
137
+ patch = ToriiUpdateUserInput(name="Ada", phone=None)
138
+ torii.users.update(user_id, patch)
139
+ ```
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,107 @@
1
+ # torii-backend
2
+
3
+ Backend SDK for [torii](https://torii.so) — verify end-user JWTs without a per-request round trip and manage users from your Python server.
4
+
5
+ > **v0.x — API may still change.**
6
+
7
+ ## Setup
8
+
9
+ 1. Sign in to [app.torii.so](https://app.torii.so) and from your dashboard copy:
10
+ - your **issuer URL** (e.g. `https://acme.torii.so`)
11
+ - a **secret key** (`sk_test_…` for development, `sk_live_…` for production)
12
+
13
+ 2. Install the SDK:
14
+
15
+ ```sh
16
+ pip install torii-backend
17
+ # or, with the FastAPI dependency adapter:
18
+ pip install "torii-backend[fastapi]"
19
+ ```
20
+
21
+ Python 3.9+.
22
+
23
+ 3. Verify an end-user JWT:
24
+
25
+ ```python
26
+ from torii_backend import verify_token
27
+
28
+ auth = verify_token(token, issuer="https://acme.torii.so")
29
+ print(auth.user_id, auth.environment_id, auth.email_verified)
30
+ ```
31
+
32
+ The first call fetches the issuer's JWKS; subsequent calls reuse the cache and rotate keys automatically (handled by [`PyJWT`](https://pyjwt.readthedocs.io/)). No round trip per request.
33
+
34
+ 4. Call the backend REST API:
35
+
36
+ ```python
37
+ import os
38
+ from torii_backend import create_torii_client
39
+
40
+ torii = create_torii_client(secret_key=os.environ["TORII_SECRET_KEY"])
41
+ user = torii.users.get(user_id)
42
+ ```
43
+
44
+ Default base URL is `https://api.torii.so`. Override with `api_url=` for staging or self-hosted.
45
+
46
+ ## FastAPI
47
+
48
+ ```python
49
+ from fastapi import Depends, FastAPI
50
+ from torii_backend.fastapi import require_auth
51
+
52
+ app = FastAPI()
53
+ auth_dep = require_auth(issuer="https://acme.torii.so")
54
+
55
+ @app.get("/me")
56
+ def me(auth = Depends(auth_dep)):
57
+ return {"user_id": auth.user_id}
58
+ ```
59
+
60
+ ## Backend API
61
+
62
+ ```python
63
+ page = torii.users.list(limit=50)
64
+ user = torii.users.create(email="x@y.com")
65
+ torii.users.ban(user.id)
66
+
67
+ sessions = torii.sessions.list_for_user(user.id)
68
+ torii.sessions.revoke_all_for_user(user.id)
69
+ ```
70
+
71
+ ### PATCH semantics (`users.update`)
72
+
73
+ `users.update` is a tri-state PATCH: each updatable field can be **set**, **cleared**, or **left alone**. Pass only the kwargs you want on the wire — pydantic v2's `model_fields_set` tracks which fields were explicitly provided, and the SDK serializes via `model_dump(exclude_unset=True)` so untouched fields never appear in the request body.
74
+
75
+ ```python
76
+ torii.users.update(
77
+ user_id,
78
+ name="Ada", # → server updates name
79
+ phone=None, # → server clears phone (sent as JSON null)
80
+ # address not passed → server leaves alone
81
+ )
82
+ ```
83
+
84
+ Wire body for the call above:
85
+
86
+ ```json
87
+ { "name": "Ada", "phone": null }
88
+ ```
89
+
90
+ | Call | Field on wire | Server effect |
91
+ | ------------------------------------- | ----------------- | ------------------ |
92
+ | `users.update(id, name="Ada")` | `"name": "Ada"` | update `name` |
93
+ | `users.update(id, phone=None)` | `"phone": null` | clear `phone` |
94
+ | `users.update(id)` (no field kwargs) | omitted | leave alone |
95
+
96
+ You can also build a request explicitly — useful when assembling the patch dynamically:
97
+
98
+ ```python
99
+ from torii_backend import ToriiUpdateUserInput
100
+
101
+ patch = ToriiUpdateUserInput(name="Ada", phone=None)
102
+ torii.users.update(user_id, patch)
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
3
+ "spaces": 2,
4
+ "generator-cli": {
5
+ "version": "7.22.0"
6
+ }
7
+ }
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "torii-backend"
3
+ version = "0.0.2"
4
+ description = "Backend SDK for torii — verify JWTs, manage users, react to events from your Python server."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "GOOD Code ApS" }]
9
+ keywords = ["torii", "auth", "jwt", "oidc", "backend"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = [
22
+ "pyjwt[crypto]>=2.8,<3.0",
23
+ # Transitive deps of the openapi-generator output under
24
+ # `torii_backend.generated`. We name them here explicitly so the
25
+ # public dependency surface matches the published wheel.
26
+ "urllib3>=2.1.0,<3.0.0",
27
+ "pydantic>=2.11",
28
+ "python-dateutil>=2.8.2",
29
+ "typing-extensions>=4.7.1",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ fastapi = ["fastapi>=0.100"]
34
+ dev = [
35
+ "pytest>=8.0",
36
+ "pytest-asyncio>=0.23",
37
+ "ruff>=0.6",
38
+ "mypy>=1.10",
39
+ "fastapi>=0.100",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://torii.so"
44
+ Source = "https://github.com/Torii-ApS/torii-sdk-python"
45
+ Issues = "https://github.com/Torii-ApS/torii-sdk-python/issues"
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/torii_backend"]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+ target-version = "py39"
57
+ extend-exclude = ["src/torii_backend/generated"]
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I", "UP", "B", "SIM"]
61
+ ignore = ["E501"] # line length is handled by formatter
62
+
63
+ [tool.mypy]
64
+ python_version = "3.9"
65
+ strict = true
66
+ warn_unused_ignores = true
67
+ warn_redundant_casts = true
68
+ exclude = ["src/torii_backend/generated/"]
69
+
70
+ [[tool.mypy.overrides]]
71
+ module = "torii_backend.generated.*"
72
+ ignore_errors = true
73
+
74
+ [[tool.mypy.overrides]]
75
+ module = "tests.*"
76
+ disallow_untyped_defs = false
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ python_files = "test_*.py"
81
+ asyncio_mode = "auto"
@@ -0,0 +1 @@
1
+ {"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:52334","description":"Generated server url"}],"tags":[{"name":"Server Users","description":"Server user management endpoints (secret key auth)"},{"name":"Server Sessions","description":"Server session management endpoints (secret key auth)"}],"paths":{"/api/server/v1/users":{"post":{"tags":["Server Users"],"summary":"Create user","description":"Creates an end-user in your environment. All body fields are optional; supply at minimum an email if you want the user to be able to sign in via email + password.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"201":{"description":"The created user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"400":{"description":"Invalid body (e.g. weak password, malformed email).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"409":{"description":"Another user with this email already exists in the environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/{userId}/unban":{"post":{"tags":["Server Users"],"summary":"Unban user","description":"Reverses a previous ban. The user can sign in again on next request.","operationId":"unbanUser","parameters":[{"name":"userId","in":"path","description":"Identifier of the user to unban.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"200":{"description":"The (now active) user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"404":{"description":"No user with this id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/{userId}/ban":{"post":{"tags":["Server Users"],"summary":"Ban user","description":"Marks the user as banned and revokes all their active sessions.","operationId":"banUser","parameters":[{"name":"userId","in":"path","description":"Identifier of the user to ban.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"200":{"description":"The (now banned) user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"404":{"description":"No user with this id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/search":{"post":{"tags":["Server Users"],"summary":"Search users","description":"Returns a cursor-paginated page of end-users in the environment matching the optional filters. Filters use the same tri-state PATCH semantics as `UpdateUserRequest`: omit a field to skip that filter, send a value to require it, send null to require null. Uses POST so the filter body can be sent without URL-encoding.","operationId":"searchUsers","parameters":[{"name":"limit","in":"query","description":"Maximum number of items in the returned page (default 20).","required":false,"schema":{"type":"integer","format":"int32","default":20},"example":50},{"name":"cursor","in":"query","description":"Opaque cursor returned by the previous page's `nextCursor`. Omit to fetch the first page.","required":false,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerUserSearchRequest"}}}},"responses":{"200":{"description":"Page of matching users.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPageResponseUserResponse"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/{userId}":{"get":{"tags":["Server Users"],"summary":"Get user","description":"Returns the full profile for one end-user.","operationId":"getUser","parameters":[{"name":"userId","in":"path","description":"Identifier of the user to fetch.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"200":{"description":"The user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"404":{"description":"No user with this id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}},"delete":{"tags":["Server Users"],"summary":"Delete user","description":"Soft-deletes the user. Not idempotent at the HTTP layer: the authorization grant for the user is revoked on the first successful delete, so a subsequent DELETE for the same id returns 403 rather than 204. Treat 403 from a retry as a confirmation that the user is already deleted.","operationId":"deleteUser","parameters":[{"name":"userId","in":"path","description":"Identifier of the user to delete.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"204":{"description":"User deleted."},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"404":{"description":"No user with this id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}},"patch":{"tags":["Server Users"],"summary":"Update user","description":"Partial update with tri-state PATCH semantics. Every field in `UpdateUserRequest` is tri-state: omit the key to leave the field unchanged, send a non-null value to set it, or send JSON null to clear it.","operationId":"updateUser","parameters":[{"name":"userId","in":"path","description":"Identifier of the user to update.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"The updated user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"400":{"description":"Invalid body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"404":{"description":"No user with this id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/{userId}/sessions":{"get":{"tags":["Server Sessions"],"summary":"List user sessions","description":"Returns all active (unexpired, unrevoked) sessions for the user, ordered by most recently used.","operationId":"listSessions","parameters":[{"name":"userId","in":"path","description":"Identifier of the user whose sessions to list.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"200":{"description":"All active sessions for the user.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserSessionResponse"}}}}},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}},"delete":{"tags":["Server Sessions"],"summary":"Revoke all sessions","description":"Immediately revokes every active session for the user. Idempotent.","operationId":"revokeAllSessions","parameters":[{"name":"userId","in":"path","description":"Identifier of the user whose sessions to revoke.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"}],"responses":{"204":{"description":"Sessions revoked."},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}},"/api/server/v1/users/{userId}/sessions/{sessionId}":{"delete":{"tags":["Server Sessions"],"summary":"Revoke specific session","description":"Revokes a single session by id. Idempotent: returns 204 even if the session was already revoked or expired.","operationId":"revokeSession","parameters":[{"name":"userId","in":"path","description":"Identifier of the user who owns the session.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a73-8b00-7000-8000-000000000000"},{"name":"sessionId","in":"path","description":"Identifier of the session to revoke.","required":true,"schema":{"type":"string","format":"uuid"},"example":"01931a74-1234-7000-8000-000000000000"}],"responses":{"204":{"description":"Session revoked."},"401":{"description":"Missing or invalid secret key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}},"403":{"description":"User or session belongs to a different environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetail"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"type":"object","description":"Request body for creating an end-user in your environment. All fields are optional; supply at minimum an email if you want the user to be able to sign in via email + password.","properties":{"email":{"type":["string","null"],"description":"Primary email for the new user. If omitted, the user is created without a sign-in identity.","example":"ada@example.com"},"password":{"type":["string","null"],"description":"Initial password. Subject to the environment's password policy. Omit to create a passwordless user (e.g. social-only).","example":"correct horse battery staple"},"name":{"type":["string","null"],"description":"Display name to seed on the profile.","example":"Ada Lovelace"},"phone":{"type":["string","null"],"description":"Phone number to seed on the profile.","example":"+15555550100"},"address":{"type":["string","null"],"description":"Postal address to seed on the profile.","example":"221B Baker Street, London"},"dateOfBirth":{"type":["string","null"],"format":"date","description":"Date of birth in ISO-8601 (YYYY-MM-DD).","example":"1815-12-10"}}},"ProblemDetail":{"type":"object","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer","format":"int32"},"detail":{"type":"string"},"instance":{"type":"string","format":"uri"},"properties":{"type":"object","additionalProperties":{}}}},"UserResponse":{"type":"object","description":"An end-user belonging to one of your environments.","properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for this user.","example":"01931a73-8b00-7000-8000-000000000000"},"environmentId":{"type":"string","format":"uuid","description":"Identifier of the environment this user belongs to.","example":"01931a72-0000-7000-8000-000000000000"},"name":{"type":["string","null"],"description":"Full name on the profile, if any.","example":"Ada Lovelace"},"phone":{"type":["string","null"],"description":"Phone number on the profile, if any. Not guaranteed to be verified.","example":"+15555550100"},"locale":{"type":["string","null"],"description":"Preferred locale for emails and UI messages.","enum":["en","da"]},"address":{"type":["string","null"],"description":"Free-form address string, if provided.","example":"221B Baker Street, London"},"dateOfBirth":{"type":["string","null"],"format":"date","description":"Date of birth in ISO-8601 (YYYY-MM-DD), if provided.","example":"1815-12-10"},"status":{"type":"string","description":"Lifecycle status of the user (e.g. active, banned).","enum":["pending_verification","active","banned","deleted"]},"createdAt":{"type":"string","format":"date-time","description":"When this user was created (ISO-8601 UTC).","example":"2026-05-16T09:30:00Z"},"updatedAt":{"type":"string","format":"date-time","description":"When this user was last modified (ISO-8601 UTC).","example":"2026-05-16T10:00:00Z"},"email":{"type":["string","null"],"description":"Primary email on the profile, if any. Not guaranteed to be verified.","example":"ada@example.com"},"deletedAt":{"type":["string","null"],"format":"date-time","description":"When this user was deleted, if soft-deleted. Null for active users.","example":"2026-05-20T12:00:00Z"}},"required":["createdAt","environmentId","id","status","updatedAt"]},"ServerUserSearchRequest":{"type":"object","description":"Optional filter body for `POST /users/search`. Every field is tri-state: omit to skip that filter, send a value to require it. Fields whose inner type is nullable (currently `name`, `email`) additionally accept JSON null to filter for users where that column is null; the non-nullable `statuses` field rejects null.","properties":{"name":{"type":["string","null"],"description":"Filter by name (case-insensitive substring match). Send null to require users with no name.","example":"Ada"},"email":{"type":["string","null"],"description":"Filter by primary email (case-insensitive substring match). Send null to require users with no email.","example":"@example.com"},"statuses":{"type":"array","description":"Filter by user status. Returns users matching any of the supplied statuses.","items":{"type":"string","enum":["pending_verification","active","banned","deleted"]},"uniqueItems":true},"createdAfter":{"type":["string","null"],"format":"date-time","description":"Only return users created at or after this instant (ISO-8601 UTC).","example":"2026-01-01T00:00:00Z"},"createdBefore":{"type":["string","null"],"format":"date-time","description":"Only return users created at or before this instant (ISO-8601 UTC).","example":"2026-12-31T23:59:59Z"}}},"CursorPageResponseUserResponse":{"type":"object","description":"A single page of results in a cursor-paginated list. Pass `nextCursor` as the `cursor` query parameter to fetch the following page.","properties":{"items":{"type":"array","description":"Items in this page, in stable order.","items":{"$ref":"#/components/schemas/UserResponse"}},"nextCursor":{"type":["string","null"],"format":"uuid","description":"Cursor to pass to fetch the next page. Null when this is the last page.","example":"01931a73-8b00-7000-8000-000000000000"},"hasMore":{"type":"boolean","description":"True if more pages are available (equivalent to `nextCursor != null`).","example":true}},"required":["hasMore","items"]},"UpdateUserRequest":{"type":"object","description":"PATCH body for updating an end-user. Every field is tri-state: omit the key entirely to leave the field unchanged, send a non-null value to set it, or send JSON null to clear it.","properties":{"name":{"type":["string","null"],"description":"New display name. Send null to clear; omit to leave unchanged.","example":"Ada Lovelace"},"phone":{"type":["string","null"],"description":"New phone number. Send null to clear; omit to leave unchanged.","example":"+15555550100"},"locale":{"type":["string","null"],"description":"New preferred locale. Send null to clear; omit to leave unchanged.","enum":["en","da"]},"address":{"type":["string","null"],"description":"New postal address. Send null to clear; omit to leave unchanged.","example":"221B Baker Street, London"},"dateOfBirth":{"type":["string","null"],"format":"date","description":"New date of birth (YYYY-MM-DD). Send null to clear; omit to leave unchanged.","example":"1815-12-10"}}},"UserSessionResponse":{"type":"object","description":"An active end-user session in your environment.","properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for this session.","example":"01931a74-1234-7000-8000-000000000000"},"userId":{"type":"string","format":"uuid","description":"Identifier of the end-user this session belongs to.","example":"01931a73-8b00-7000-8000-000000000000"},"environmentId":{"type":"string","format":"uuid","description":"Identifier of the environment this session belongs to.","example":"01931a72-0000-7000-8000-000000000000"},"userAgent":{"type":["string","null"],"description":"Raw User-Agent string captured when the session was created.","example":"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_0) AppleWebKit/537.36"},"ipAddress":{"type":["string","null"],"description":"IP address captured when the session was created.","example":"203.0.113.42"},"createdAt":{"type":"string","format":"date-time","description":"When this session was created (ISO-8601 UTC).","example":"2026-05-16T09:30:00Z"},"expiresAt":{"type":"string","format":"date-time","description":"When this session expires (ISO-8601 UTC).","example":"2026-05-23T09:30:00Z"},"lastUsedAt":{"type":"string","format":"date-time","description":"When this session was last seen by the API (ISO-8601 UTC).","example":"2026-05-16T11:42:00Z"}},"required":["createdAt","environmentId","expiresAt","id","lastUsedAt","userId"]}}}}