everscribe 0.1.0__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 (44) hide show
  1. everscribe-0.1.0/.github/workflows/ci.yml +24 -0
  2. everscribe-0.1.0/.gitignore +31 -0
  3. everscribe-0.1.0/LICENSE +21 -0
  4. everscribe-0.1.0/PKG-INFO +91 -0
  5. everscribe-0.1.0/README.md +50 -0
  6. everscribe-0.1.0/assets/everscribe.svg +7 -0
  7. everscribe-0.1.0/assets/python.svg +1 -0
  8. everscribe-0.1.0/pyproject.toml +98 -0
  9. everscribe-0.1.0/src/everscribe/__init__.py +49 -0
  10. everscribe-0.1.0/src/everscribe/asgi.py +155 -0
  11. everscribe-0.1.0/src/everscribe/client.py +102 -0
  12. everscribe-0.1.0/src/everscribe/django.py +181 -0
  13. everscribe-0.1.0/src/everscribe/event/__init__.py +71 -0
  14. everscribe-0.1.0/src/everscribe/event/context.py +349 -0
  15. everscribe-0.1.0/src/everscribe/event/event.py +114 -0
  16. everscribe-0.1.0/src/everscribe/event/origin.py +60 -0
  17. everscribe-0.1.0/src/everscribe/event/redact.py +111 -0
  18. everscribe-0.1.0/src/everscribe/event/types.py +141 -0
  19. everscribe-0.1.0/src/everscribe/event/wire.py +126 -0
  20. everscribe-0.1.0/src/everscribe/flask.py +184 -0
  21. everscribe-0.1.0/src/everscribe/grpc.py +429 -0
  22. everscribe-0.1.0/src/everscribe/minter/__init__.py +31 -0
  23. everscribe-0.1.0/src/everscribe/minter/client.py +102 -0
  24. everscribe-0.1.0/src/everscribe/minter/columns.py +26 -0
  25. everscribe-0.1.0/src/everscribe/minter/errors.py +17 -0
  26. everscribe-0.1.0/src/everscribe/minter/options.py +135 -0
  27. everscribe-0.1.0/src/everscribe/py.typed +0 -0
  28. everscribe-0.1.0/src/everscribe/recorder/__init__.py +55 -0
  29. everscribe-0.1.0/src/everscribe/recorder/base.py +33 -0
  30. everscribe-0.1.0/src/everscribe/recorder/buffered.py +269 -0
  31. everscribe-0.1.0/src/everscribe/recorder/errors.py +42 -0
  32. everscribe-0.1.0/src/everscribe/recorder/factory.py +64 -0
  33. everscribe-0.1.0/src/everscribe/recorder/http.py +128 -0
  34. everscribe-0.1.0/tests/test_asgi.py +215 -0
  35. everscribe-0.1.0/tests/test_buffered_recorder.py +281 -0
  36. everscribe-0.1.0/tests/test_client.py +97 -0
  37. everscribe-0.1.0/tests/test_context.py +296 -0
  38. everscribe-0.1.0/tests/test_django.py +212 -0
  39. everscribe-0.1.0/tests/test_event.py +291 -0
  40. everscribe-0.1.0/tests/test_flask.py +213 -0
  41. everscribe-0.1.0/tests/test_grpc.py +639 -0
  42. everscribe-0.1.0/tests/test_http_recorder.py +192 -0
  43. everscribe-0.1.0/tests/test_minter.py +261 -0
  44. everscribe-0.1.0/tests/test_origin.py +51 -0
@@ -0,0 +1,24 @@
1
+ name: ci
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: '3.12'
17
+ - name: Install
18
+ run: pip install -e ".[dev,fastapi,flask,django,grpc]"
19
+ - name: Lint
20
+ run: ruff check .
21
+ - name: Typecheck
22
+ run: mypy src
23
+ - name: Test
24
+ run: pytest -q
@@ -0,0 +1,31 @@
1
+ # Byte-compiled / optimized / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Test / coverage / type-check caches
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ htmlcov/
23
+
24
+ # Editors / OS
25
+ .DS_Store
26
+ .idea/
27
+ .vscode/
28
+
29
+ # subagent scratch reports
30
+ .*-report.md
31
+ .grpc-spike.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Everscribe
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,91 @@
1
+ Metadata-Version: 2.5
2
+ Name: everscribe
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Everscribe audit-log API
5
+ Project-URL: Homepage, https://everscribe.io
6
+ Project-URL: Repository, https://github.com/everscribe/sdk-python
7
+ Author: Everscribe
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: audit,audit-log,everscribe,observability,sdk
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Provides-Extra: dev
23
+ Requires-Dist: django>=4; extra == 'dev'
24
+ Requires-Dist: flask>=2; extra == 'dev'
25
+ Requires-Dist: grpcio>=1.60; extra == 'dev'
26
+ Requires-Dist: httpx>=0.24; extra == 'dev'
27
+ Requires-Dist: mypy>=1.5; extra == 'dev'
28
+ Requires-Dist: pytest>=7; extra == 'dev'
29
+ Requires-Dist: ruff==0.16.0; extra == 'dev'
30
+ Requires-Dist: starlette>=0.27; extra == 'dev'
31
+ Requires-Dist: types-grpcio; extra == 'dev'
32
+ Provides-Extra: django
33
+ Requires-Dist: django>=4; extra == 'django'
34
+ Provides-Extra: fastapi
35
+ Requires-Dist: starlette>=0.27; extra == 'fastapi'
36
+ Provides-Extra: flask
37
+ Requires-Dist: flask>=2; extra == 'flask'
38
+ Provides-Extra: grpc
39
+ Requires-Dist: grpcio>=1.60; extra == 'grpc'
40
+ Description-Content-Type: text/markdown
41
+
42
+ <div align="center">
43
+
44
+ <p>
45
+ <img src="assets/everscribe.svg" alt="Everscribe" height="64" align="middle">
46
+ &nbsp;&nbsp;<b>+</b>&nbsp;&nbsp;
47
+ <img src="assets/python.svg" alt="Python" height="56" align="middle">
48
+ </p>
49
+
50
+ <p>
51
+ <a href="https://pypi.org/project/everscribe/"><img src="https://img.shields.io/pypi/v/everscribe.svg" alt="PyPI"></a>
52
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
53
+ </p>
54
+
55
+ # sdk-python
56
+
57
+ Python SDK for the [Everscribe](https://everscribe.io) API. It exposes **middleware** that records an audit event per request, a **recorder** for writing append-only audit events directly, and a **minter** for issuing short-lived browser tokens for [embeddable components](https://github.com/everscribe/components).
58
+
59
+ </div>
60
+
61
+ ## 📖 Setup Documentation
62
+
63
+ | Setup link | What it is | Pick it when |
64
+ |---|---|---|
65
+ | [Hosted AI agent](https://everscribe.io/docs/quickstart/hosted-ui) | You install our GitHub App. Our server-side agent walks your repo, proposes a plan, and opens a PR you review. | You want zero hand-wiring and are fine granting Everscribe read access to your repo. Paid feature. |
66
+ | [BYOK Claude Code skill](https://everscribe.io/docs/quickstart/skill) | Install the [`es` CLI](https://github.com/everscribe/cli) and run `es skills download setup`. That drops a skill into your Claude Code skills directory, and your own local Claude agent does the setup. Same behavior as the hosted agent, but against *your* Anthropic key, and your source never leaves your machine. | You do not want to grant Everscribe read access to your repo, and/or you want a free option and already have Claude Code. |
67
+ | [Wire it up yourself](https://everscribe.io/docs/sdks/python-install) | Install the SDK and add the calls by hand. | You want full control, or you are instrumenting a small surface. |
68
+
69
+ ## Supported frameworks
70
+
71
+ - `FastAPI`
72
+ - `Starlette`
73
+ - `Flask`
74
+ - `Django`
75
+
76
+ > [!NOTE]
77
+ > Interceptors available for gRPC servers.
78
+
79
+ Your framework missing? [Open an issue](https://github.com/everscribe/sdk-python/issues).
80
+
81
+ ## Additional resources
82
+
83
+ - [Runnable examples](https://github.com/everscribe/examples): full-stack runnable
84
+ apps for every Everscribe SDK.
85
+
86
+ - [AI-assisted setup demo](https://github.com/everscribe/ai-assisted-setup-demo): Sample repo for trying AI setup against something disposable.
87
+
88
+ - [Everscribe CLI](https://github.com/everscribe/cli): CLI for downloading the Claude skill and managing projects, API keys, events and more.
89
+
90
+ - [Embeddable components](https://github.com/everscribe/components): drop-in audit
91
+ trail UI for single and multi-tenant apps.
@@ -0,0 +1,50 @@
1
+ <div align="center">
2
+
3
+ <p>
4
+ <img src="assets/everscribe.svg" alt="Everscribe" height="64" align="middle">
5
+ &nbsp;&nbsp;<b>+</b>&nbsp;&nbsp;
6
+ <img src="assets/python.svg" alt="Python" height="56" align="middle">
7
+ </p>
8
+
9
+ <p>
10
+ <a href="https://pypi.org/project/everscribe/"><img src="https://img.shields.io/pypi/v/everscribe.svg" alt="PyPI"></a>
11
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
12
+ </p>
13
+
14
+ # sdk-python
15
+
16
+ Python SDK for the [Everscribe](https://everscribe.io) API. It exposes **middleware** that records an audit event per request, a **recorder** for writing append-only audit events directly, and a **minter** for issuing short-lived browser tokens for [embeddable components](https://github.com/everscribe/components).
17
+
18
+ </div>
19
+
20
+ ## 📖 Setup Documentation
21
+
22
+ | Setup link | What it is | Pick it when |
23
+ |---|---|---|
24
+ | [Hosted AI agent](https://everscribe.io/docs/quickstart/hosted-ui) | You install our GitHub App. Our server-side agent walks your repo, proposes a plan, and opens a PR you review. | You want zero hand-wiring and are fine granting Everscribe read access to your repo. Paid feature. |
25
+ | [BYOK Claude Code skill](https://everscribe.io/docs/quickstart/skill) | Install the [`es` CLI](https://github.com/everscribe/cli) and run `es skills download setup`. That drops a skill into your Claude Code skills directory, and your own local Claude agent does the setup. Same behavior as the hosted agent, but against *your* Anthropic key, and your source never leaves your machine. | You do not want to grant Everscribe read access to your repo, and/or you want a free option and already have Claude Code. |
26
+ | [Wire it up yourself](https://everscribe.io/docs/sdks/python-install) | Install the SDK and add the calls by hand. | You want full control, or you are instrumenting a small surface. |
27
+
28
+ ## Supported frameworks
29
+
30
+ - `FastAPI`
31
+ - `Starlette`
32
+ - `Flask`
33
+ - `Django`
34
+
35
+ > [!NOTE]
36
+ > Interceptors available for gRPC servers.
37
+
38
+ Your framework missing? [Open an issue](https://github.com/everscribe/sdk-python/issues).
39
+
40
+ ## Additional resources
41
+
42
+ - [Runnable examples](https://github.com/everscribe/examples): full-stack runnable
43
+ apps for every Everscribe SDK.
44
+
45
+ - [AI-assisted setup demo](https://github.com/everscribe/ai-assisted-setup-demo): Sample repo for trying AI setup against something disposable.
46
+
47
+ - [Everscribe CLI](https://github.com/everscribe/cli): CLI for downloading the Claude skill and managing projects, API keys, events and more.
48
+
49
+ - [Embeddable components](https://github.com/everscribe/components): drop-in audit
50
+ trail UI for single and multi-tenant apps.
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
2
+ <rect x="0" y="0" width="200" height="200" fill="#1e3a5f"/>
3
+ <!-- { ES } -->
4
+ <text x="100" y="124" font-family="'Helvetica Neue', 'Arial', sans-serif"
5
+ font-weight="500" font-size="72" fill="#f0f4f8"
6
+ text-anchor="middle">{ ES }</text>
7
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><linearGradient id="python-original-a" gradientUnits="userSpaceOnUse" x1="70.252" y1="1237.476" x2="170.659" y2="1151.089" gradientTransform="matrix(.563 0 0 -.568 -29.215 707.817)"><stop offset="0" stop-color="#5A9FD4"/><stop offset="1" stop-color="#306998"/></linearGradient><linearGradient id="python-original-b" gradientUnits="userSpaceOnUse" x1="209.474" y1="1098.811" x2="173.62" y2="1149.537" gradientTransform="matrix(.563 0 0 -.568 -29.215 707.817)"><stop offset="0" stop-color="#FFD43B"/><stop offset="1" stop-color="#FFE873"/></linearGradient><path fill="url(#python-original-a)" d="M63.391 1.988c-4.222.02-8.252.379-11.8 1.007-10.45 1.846-12.346 5.71-12.346 12.837v9.411h24.693v3.137H29.977c-7.176 0-13.46 4.313-15.426 12.521-2.268 9.405-2.368 15.275 0 25.096 1.755 7.311 5.947 12.519 13.124 12.519h8.491V67.234c0-8.151 7.051-15.34 15.426-15.34h24.665c6.866 0 12.346-5.654 12.346-12.548V15.833c0-6.693-5.646-11.72-12.346-12.837-4.244-.706-8.645-1.027-12.866-1.008zM50.037 9.557c2.55 0 4.634 2.117 4.634 4.721 0 2.593-2.083 4.69-4.634 4.69-2.56 0-4.633-2.097-4.633-4.69-.001-2.604 2.073-4.721 4.633-4.721z" transform="translate(0 10.26)"/><path fill="url(#python-original-b)" d="M91.682 28.38v10.966c0 8.5-7.208 15.655-15.426 15.655H51.591c-6.756 0-12.346 5.783-12.346 12.549v23.515c0 6.691 5.818 10.628 12.346 12.547 7.816 2.297 15.312 2.713 24.665 0 6.216-1.801 12.346-5.423 12.346-12.547v-9.412H63.938v-3.138h37.012c7.176 0 9.852-5.005 12.348-12.519 2.578-7.735 2.467-15.174 0-25.096-1.774-7.145-5.161-12.521-12.348-12.521h-9.268zM77.809 87.927c2.561 0 4.634 2.097 4.634 4.692 0 2.602-2.074 4.719-4.634 4.719-2.55 0-4.633-2.117-4.633-4.719 0-2.595 2.083-4.692 4.633-4.692z" transform="translate(0 10.26)"/><radialGradient id="python-original-c" cx="1825.678" cy="444.45" r="26.743" gradientTransform="matrix(0 -.24 -1.055 0 532.979 557.576)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#B8B8B8" stop-opacity=".498"/><stop offset="1" stop-color="#7F7F7F" stop-opacity="0"/></radialGradient><path opacity=".444" fill="url(#python-original-c)" d="M97.309 119.597c0 3.543-14.816 6.416-33.091 6.416-18.276 0-33.092-2.873-33.092-6.416 0-3.544 14.815-6.417 33.092-6.417 18.275 0 33.091 2.872 33.091 6.417z"/></svg>
@@ -0,0 +1,98 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ # Single version literal: src/everscribe/__init__.py exports __version__ in
6
+ # __all__, so a second copy here would drift and ship a wrong number.
7
+ [tool.hatch.version]
8
+ path = "src/everscribe/__init__.py"
9
+
10
+ [project]
11
+ name = "everscribe"
12
+ dynamic = ["version"]
13
+ description = "Python SDK for the Everscribe audit-log API"
14
+ readme = "README.md"
15
+ requires-python = ">=3.10"
16
+ license = { text = "MIT" }
17
+ authors = [{ name = "Everscribe" }]
18
+ keywords = ["everscribe", "audit-log", "audit", "sdk", "observability"]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Typing :: Typed",
30
+ ]
31
+ # Core is intentionally zero-dependency: uuid, json, urllib, threading, queue,
32
+ # and contextvars are all part of the Python standard library.
33
+ dependencies = []
34
+
35
+ [project.optional-dependencies]
36
+ # ASGI adapter (FastAPI, Starlette, and any other ASGI app). FastAPI bundles
37
+ # Starlette, so depending on Starlette alone covers both.
38
+ fastapi = ["starlette>=0.27"]
39
+ flask = ["flask>=2"]
40
+ django = ["django>=4"]
41
+ grpc = ["grpcio>=1.60"]
42
+ dev = [
43
+ "pytest>=7",
44
+ "starlette>=0.27",
45
+ "httpx>=0.24",
46
+ "flask>=2",
47
+ "django>=4",
48
+ "grpcio>=1.60",
49
+ "types-grpcio",
50
+ "mypy>=1.5",
51
+ # Pinned exactly: ruff's default rule set widens between releases, so an
52
+ # unpinned range silently fails CI on code that never changed. Bump this
53
+ # deliberately and re-run `ruff check` when adopting a new version.
54
+ "ruff==0.16.0",
55
+ ]
56
+
57
+ [project.urls]
58
+ Homepage = "https://everscribe.io"
59
+ Repository = "https://github.com/everscribe/sdk-python"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["src/everscribe"]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ addopts = "-ra"
67
+ filterwarnings = [
68
+ # Starlette's TestClient warns about the installed httpx version; external,
69
+ # test-only, and unrelated to the SDK.
70
+ "ignore:Using .httpx. with .starlette.testclient. is deprecated",
71
+ ]
72
+
73
+ [tool.mypy]
74
+ python_version = "3.10"
75
+ strict = true
76
+ files = ["src/everscribe"]
77
+
78
+ [[tool.mypy.overrides]]
79
+ # Django ships no py.typed marker / inline stubs (unlike Starlette, which
80
+ # this project's ASGI adapter depends on cleanly). Only the adapter's own
81
+ # code is type-checked strictly; Django's surface is treated as untyped.
82
+ module = "django.*"
83
+ ignore_missing_imports = true
84
+
85
+ [tool.ruff]
86
+ line-length = 100
87
+ src = ["src", "tests"]
88
+
89
+ [tool.ruff.lint]
90
+ # Select explicitly rather than inheriting ruff's default set, which widens
91
+ # between releases and would reintroduce the drift the pinned version guards
92
+ # against. E is scoped to the logical-error subset (E4/E7/E9); line-length and
93
+ # whitespace formatting (E1/E2/E5) are left out, since this repo gates on a
94
+ # linter, not a formatter, matching the Go SDK's no-gofmt-check stance.
95
+ select = ["E4", "E7", "E9", "F", "I", "B", "C4", "DTZ", "UP", "RUF", "BLE"]
96
+ # __all__ blocks are grouped by section with comments on purpose; RUF022's
97
+ # isort-style sort scatters those groups. Keep the human-readable grouping.
98
+ ignore = ["RUF022"]
@@ -0,0 +1,49 @@
1
+ """Everscribe Python SDK.
2
+
3
+ Python SDK for the Everscribe audit-log API. Two coordinated surfaces:
4
+
5
+ - :mod:`everscribe.recorder` - append-only event ingest.
6
+ - :mod:`everscribe.minter` - short-lived embed tokens for frontend audit views.
7
+
8
+ Bind credentials once with :func:`new` (or :func:`new_from_env`) and create
9
+ per-surface subclients from the returned :class:`Client`::
10
+
11
+ import everscribe
12
+
13
+ es = everscribe.new(project_id, api_key)
14
+ rec = es.new_recorder()
15
+ rec.record(everscribe.Event("user.login"))
16
+
17
+ The core is zero-dependency (standard library only). The optional ASGI adapter
18
+ lives in :mod:`everscribe.asgi` (install the ``fastapi`` extra).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from . import event, minter, recorder
24
+ from .client import (
25
+ EVERSCRIBE_API_KEY,
26
+ EVERSCRIBE_PROJECT_ID,
27
+ Client,
28
+ new,
29
+ new_from_env,
30
+ )
31
+ from .event import Event
32
+
33
+ __version__ = "0.1.0"
34
+
35
+ __all__ = [
36
+ "__version__",
37
+ # Root client
38
+ "Client",
39
+ "new",
40
+ "new_from_env",
41
+ "EVERSCRIBE_PROJECT_ID",
42
+ "EVERSCRIBE_API_KEY",
43
+ # Event model
44
+ "Event",
45
+ # Subpackages
46
+ "event",
47
+ "recorder",
48
+ "minter",
49
+ ]
@@ -0,0 +1,155 @@
1
+ """ASGI middleware for FastAPI / Starlette (and any ASGI app).
2
+
3
+ This adapter supplies only ASGI-specific transport bindings; the record
4
+ lifecycle itself (the request-scoped current event, dedupe, idempotency-key
5
+ stamping, and the final-outcome rule) lives in :mod:`everscribe.event` so
6
+ other framework adapters (Flask, Django, gRPC) can reuse it identically.
7
+
8
+ It:
9
+
10
+ 1. Builds an Event template (actor from ``resolve_actor``, origin from the
11
+ request) and installs it, via :func:`everscribe.event.request_scope`, in
12
+ the request-scoped context so handlers can pull fresh events with
13
+ :func:`everscribe.event.new_from_context`.
14
+ 2. Installs a status capture so :func:`everscribe.event.prepare_event` can
15
+ auto-populate the event's ``result`` from the final HTTP status when the
16
+ handler hasn't set one explicitly.
17
+ 3. Exposes a single per-request mutable Event via :func:`everscribe.event.current_event`
18
+ (re-exported here as :func:`current_event`, and available at
19
+ ``request.state.everscribe_event``) for handlers to enrich (action,
20
+ target, metadata, optionally result).
21
+ 4. If a ``recorder`` was configured, records that event once when the response
22
+ finishes via :func:`everscribe.event.end_event` - but only when the handler
23
+ set ``action`` (empty action is a no-op), and only if nothing already
24
+ claimed the dedupe flag. Auto-record failures are logged, never raised: an
25
+ audit failure must not break the user-facing response.
26
+
27
+ Mount it AFTER any session/auth middleware, since ``resolve_actor`` typically
28
+ reads identity off the request.
29
+
30
+ Requires Starlette (``pip install "everscribe[fastapi]"``).
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ from collections.abc import Callable
37
+
38
+ try:
39
+ from starlette.requests import Request
40
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
41
+ except ImportError as e: # pragma: no cover - exercised via install extras
42
+ raise ImportError(
43
+ "everscribe.asgi requires Starlette. Install it with: "
44
+ 'pip install "everscribe[fastapi]"'
45
+ ) from e
46
+
47
+ from .event import (
48
+ Event,
49
+ current_event,
50
+ end_event,
51
+ origin_from_request,
52
+ request_scope,
53
+ result_from_http_status,
54
+ )
55
+ from .event.types import Actor, Logger, Result
56
+ from .recorder import Recorder
57
+
58
+ ActorResolver = Callable[[Request], Actor]
59
+ """Derives the :class:`Actor` for a request. Typically reads session/auth
60
+ state off the Starlette ``Request``. Must not consume the request body."""
61
+
62
+ _ANONYMOUS = Actor(type="anonymous")
63
+
64
+
65
+ def event_from_request(request: Request) -> Event:
66
+ """Return the per-request event from ``request.state``, falling back to
67
+ :func:`current_event`."""
68
+ e = getattr(request.state, "everscribe_event", None)
69
+ return e if isinstance(e, Event) else current_event()
70
+
71
+
72
+ class _StatusCapture:
73
+ """Captures the response status from the ASGI ``http.response.start``
74
+ message and translates it into a :class:`Result` on demand, satisfying
75
+ :class:`everscribe.event.OutcomeCapture`.
76
+
77
+ Status 0 (nothing written yet) maps to ``outcome`` being ``None`` -
78
+ :func:`result_from_http_status` is only consulted once a status has
79
+ actually been captured.
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ self._status = 0
84
+
85
+ def set_status(self, status: int) -> None:
86
+ self._status = status
87
+
88
+ @property
89
+ def outcome(self) -> Result | None:
90
+ if self._status == 0:
91
+ return None
92
+ return result_from_http_status(self._status)
93
+
94
+
95
+ class EverscribeMiddleware:
96
+ """ASGI middleware that installs a per-request audit event and (optionally)
97
+ auto-records it on response finish.
98
+
99
+ Add it to a Starlette/FastAPI app::
100
+
101
+ app.add_middleware(
102
+ EverscribeMiddleware,
103
+ recorder=rec,
104
+ resolve_actor=lambda req: Actor(type="user", id=req.state.user_id),
105
+ )
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ app: ASGIApp,
111
+ *,
112
+ recorder: Recorder | None = None,
113
+ resolve_actor: ActorResolver | None = None,
114
+ logger: Logger | None = None,
115
+ ) -> None:
116
+ self.app = app
117
+ self.recorder = recorder
118
+ self.resolve_actor: ActorResolver = resolve_actor or (lambda req: _ANONYMOUS)
119
+ self.logger: Logger = logger or logging.getLogger("everscribe.asgi")
120
+
121
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
122
+ if scope["type"] != "http":
123
+ await self.app(scope, receive, send)
124
+ return
125
+
126
+ request = Request(scope, receive=receive)
127
+ template = Event()
128
+ template.actor = self.resolve_actor(request)
129
+ remote = request.client.host if request.client else ""
130
+ template.origin = origin_from_request(dict(request.headers), remote)
131
+
132
+ capture = _StatusCapture()
133
+
134
+ async def send_wrapper(message: Message) -> None:
135
+ if message["type"] == "http.response.start":
136
+ capture.set_status(int(message["status"]))
137
+ await send(message)
138
+
139
+ with request_scope(template, capture) as user_event:
140
+ scope.setdefault("state", {})["everscribe_event"] = user_event
141
+ try:
142
+ await self.app(scope, receive, send_wrapper)
143
+ finally:
144
+ # end_event applies the final outcome (including the "no
145
+ # response written" sentinel) and records user_event once,
146
+ # skipping it if a handler already recorded it manually.
147
+ end_event(user_event, self.recorder, self.logger)
148
+
149
+
150
+ __all__ = [
151
+ "ActorResolver",
152
+ "EverscribeMiddleware",
153
+ "current_event",
154
+ "event_from_request",
155
+ ]
@@ -0,0 +1,102 @@
1
+ """The top-level Everscribe client: binds a project's credentials once and
2
+ hands out per-surface subclients (recorder for ingest, minter for read-side
3
+ embed tokens) that share the same auth.
4
+
5
+ Typical usage::
6
+
7
+ import everscribe
8
+
9
+ es = everscribe.new(project_id, api_key) # or new_from_env()
10
+ rec = es.new_recorder()
11
+ try:
12
+ ...
13
+ finally:
14
+ rec.close()
15
+
16
+ m = es.new_minter()
17
+ token = m.mint_token(everscribe.minter.TokenOptions(...))
18
+
19
+ Customers who only need one surface can call its constructor directly -
20
+ ``everscribe.recorder.new(project_id, api_key)`` and
21
+ ``everscribe.minter.Client(project_id, api_key)`` both work and skip the
22
+ client step.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ from typing import Any
29
+
30
+ from .minter import Client as MinterClient
31
+ from .recorder import BufferedRecorder
32
+ from .recorder import new as _recorder_new
33
+
34
+ #: Environment-variable names read by :func:`new_from_env`.
35
+ EVERSCRIBE_PROJECT_ID = "EVERSCRIBE_PROJECT_ID"
36
+ EVERSCRIBE_API_KEY = "EVERSCRIBE_API_KEY"
37
+
38
+
39
+ class Client:
40
+ """Credential-bearing handle to an Everscribe project. Holds no network
41
+ state itself; the subclient constructors build per-surface clients that own
42
+ their own connections. Reuse a single Client for the lifetime of the
43
+ process - safe for concurrent use."""
44
+
45
+ def __init__(self, project_id: str, api_key: str) -> None:
46
+ """Validate and trim credentials. Raises :class:`ValueError` if either
47
+ is empty or whitespace-only, so configuration bugs surface at
48
+ construction rather than at the first network call."""
49
+ project_id = project_id.strip()
50
+ if not project_id:
51
+ raise ValueError("everscribe: project_id is empty")
52
+ api_key = api_key.strip()
53
+ if not api_key:
54
+ raise ValueError("everscribe: api_key is empty")
55
+ self.project_id = project_id
56
+ self._api_key = api_key
57
+
58
+ @classmethod
59
+ def from_env(cls) -> Client:
60
+ """Construct a Client from ``EVERSCRIBE_PROJECT_ID`` and
61
+ ``EVERSCRIBE_API_KEY``. Raises :class:`ValueError` naming the missing
62
+ variable if either is unset or empty after trimming."""
63
+ project_id = os.environ.get(EVERSCRIBE_PROJECT_ID, "").strip()
64
+ if not project_id:
65
+ raise ValueError(f"everscribe: {EVERSCRIBE_PROJECT_ID} is not set or empty")
66
+ api_key = os.environ.get(EVERSCRIBE_API_KEY, "").strip()
67
+ if not api_key:
68
+ raise ValueError(f"everscribe: {EVERSCRIBE_API_KEY} is not set or empty")
69
+ return cls(project_id, api_key)
70
+
71
+ def new_recorder(self, **opts: Any) -> BufferedRecorder:
72
+ """Return a buffered recorder for the bound project. Keyword options
73
+ forward to :func:`everscribe.recorder.new` (buffer_size, flush_interval,
74
+ base_url, ...)."""
75
+ return _recorder_new(self.project_id, self._api_key, **opts)
76
+
77
+ def new_minter(self, **opts: Any) -> MinterClient:
78
+ """Return a minter client for the bound project. Keyword options
79
+ forward to :class:`everscribe.minter.Client` (base_url,
80
+ request_timeout, ...)."""
81
+ return MinterClient(self.project_id, self._api_key, **opts)
82
+
83
+
84
+ def new(project_id: str, api_key: str) -> Client:
85
+ """Construct a :class:`Client`. Raises :class:`ValueError` on
86
+ empty/whitespace credentials."""
87
+ return Client(project_id, api_key)
88
+
89
+
90
+ def new_from_env() -> Client:
91
+ """Construct a :class:`Client` from environment variables. See
92
+ :meth:`Client.from_env`."""
93
+ return Client.from_env()
94
+
95
+
96
+ __all__ = [
97
+ "EVERSCRIBE_API_KEY",
98
+ "EVERSCRIBE_PROJECT_ID",
99
+ "Client",
100
+ "new",
101
+ "new_from_env",
102
+ ]