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.
- everscribe-0.1.0/.github/workflows/ci.yml +24 -0
- everscribe-0.1.0/.gitignore +31 -0
- everscribe-0.1.0/LICENSE +21 -0
- everscribe-0.1.0/PKG-INFO +91 -0
- everscribe-0.1.0/README.md +50 -0
- everscribe-0.1.0/assets/everscribe.svg +7 -0
- everscribe-0.1.0/assets/python.svg +1 -0
- everscribe-0.1.0/pyproject.toml +98 -0
- everscribe-0.1.0/src/everscribe/__init__.py +49 -0
- everscribe-0.1.0/src/everscribe/asgi.py +155 -0
- everscribe-0.1.0/src/everscribe/client.py +102 -0
- everscribe-0.1.0/src/everscribe/django.py +181 -0
- everscribe-0.1.0/src/everscribe/event/__init__.py +71 -0
- everscribe-0.1.0/src/everscribe/event/context.py +349 -0
- everscribe-0.1.0/src/everscribe/event/event.py +114 -0
- everscribe-0.1.0/src/everscribe/event/origin.py +60 -0
- everscribe-0.1.0/src/everscribe/event/redact.py +111 -0
- everscribe-0.1.0/src/everscribe/event/types.py +141 -0
- everscribe-0.1.0/src/everscribe/event/wire.py +126 -0
- everscribe-0.1.0/src/everscribe/flask.py +184 -0
- everscribe-0.1.0/src/everscribe/grpc.py +429 -0
- everscribe-0.1.0/src/everscribe/minter/__init__.py +31 -0
- everscribe-0.1.0/src/everscribe/minter/client.py +102 -0
- everscribe-0.1.0/src/everscribe/minter/columns.py +26 -0
- everscribe-0.1.0/src/everscribe/minter/errors.py +17 -0
- everscribe-0.1.0/src/everscribe/minter/options.py +135 -0
- everscribe-0.1.0/src/everscribe/py.typed +0 -0
- everscribe-0.1.0/src/everscribe/recorder/__init__.py +55 -0
- everscribe-0.1.0/src/everscribe/recorder/base.py +33 -0
- everscribe-0.1.0/src/everscribe/recorder/buffered.py +269 -0
- everscribe-0.1.0/src/everscribe/recorder/errors.py +42 -0
- everscribe-0.1.0/src/everscribe/recorder/factory.py +64 -0
- everscribe-0.1.0/src/everscribe/recorder/http.py +128 -0
- everscribe-0.1.0/tests/test_asgi.py +215 -0
- everscribe-0.1.0/tests/test_buffered_recorder.py +281 -0
- everscribe-0.1.0/tests/test_client.py +97 -0
- everscribe-0.1.0/tests/test_context.py +296 -0
- everscribe-0.1.0/tests/test_django.py +212 -0
- everscribe-0.1.0/tests/test_event.py +291 -0
- everscribe-0.1.0/tests/test_flask.py +213 -0
- everscribe-0.1.0/tests/test_grpc.py +639 -0
- everscribe-0.1.0/tests/test_http_recorder.py +192 -0
- everscribe-0.1.0/tests/test_minter.py +261 -0
- 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
|
everscribe-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
<b>+</b>
|
|
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
|
+
<b>+</b>
|
|
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
|
+
]
|