percolate-core 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 (49) hide show
  1. percolate_core-0.1.0/.gitignore +9 -0
  2. percolate_core-0.1.0/LICENSE +21 -0
  3. percolate_core-0.1.0/PKG-INFO +195 -0
  4. percolate_core-0.1.0/README.md +151 -0
  5. percolate_core-0.1.0/percolate_core/__init__.py +13 -0
  6. percolate_core-0.1.0/percolate_core/agentic/__init__.py +48 -0
  7. percolate_core-0.1.0/percolate_core/agentic/api.py +283 -0
  8. percolate_core-0.1.0/percolate_core/agentic/authoring.py +165 -0
  9. percolate_core-0.1.0/percolate_core/agentic/cli.py +425 -0
  10. percolate_core-0.1.0/percolate_core/agentic/contracts/__init__.py +87 -0
  11. percolate_core-0.1.0/percolate_core/agentic/contracts/agents.py +148 -0
  12. percolate_core-0.1.0/percolate_core/agentic/contracts/context.py +122 -0
  13. percolate_core-0.1.0/percolate_core/agentic/contracts/conversation.py +154 -0
  14. percolate_core-0.1.0/percolate_core/agentic/contracts/protocol.py +150 -0
  15. percolate_core-0.1.0/percolate_core/agentic/contracts/tools.py +102 -0
  16. percolate_core-0.1.0/percolate_core/agentic/db/__init__.py +32 -0
  17. percolate_core-0.1.0/percolate_core/agentic/db/client.py +174 -0
  18. percolate_core-0.1.0/percolate_core/agentic/db/repository.py +503 -0
  19. percolate_core-0.1.0/percolate_core/agentic/gateway.py +352 -0
  20. percolate_core-0.1.0/percolate_core/agentic/phases.py +110 -0
  21. percolate_core-0.1.0/percolate_core/agentic/runtime/__init__.py +41 -0
  22. percolate_core-0.1.0/percolate_core/agentic/runtime/chained_action.py +125 -0
  23. percolate_core-0.1.0/percolate_core/agentic/runtime/citations.py +75 -0
  24. percolate_core-0.1.0/percolate_core/agentic/runtime/delegation.py +137 -0
  25. percolate_core-0.1.0/percolate_core/agentic/runtime/engine.py +176 -0
  26. percolate_core-0.1.0/percolate_core/agentic/runtime/injection.py +58 -0
  27. percolate_core-0.1.0/percolate_core/agentic/runtime/persistence.py +314 -0
  28. percolate_core-0.1.0/percolate_core/agentic/runtime/protocol_adapter.py +114 -0
  29. percolate_core-0.1.0/percolate_core/agentic/sequencer.py +597 -0
  30. percolate_core-0.1.0/percolate_core/agentic/settings.py +82 -0
  31. percolate_core-0.1.0/percolate_core/agentic/testing.py +295 -0
  32. percolate_core-0.1.0/percolate_core/agentic/tools/__init__.py +24 -0
  33. percolate_core-0.1.0/percolate_core/agentic/tools/mcp.py +140 -0
  34. percolate_core-0.1.0/percolate_core/agentic/tools/openapi.py +294 -0
  35. percolate_core-0.1.0/percolate_core/agentic/tools/provider.py +89 -0
  36. percolate_core-0.1.0/percolate_core/agentic/tools/registry.py +142 -0
  37. percolate_core-0.1.0/percolate_core/agentic/tools/toolset.py +83 -0
  38. percolate_core-0.1.0/percolate_core/cli.py +64 -0
  39. percolate_core-0.1.0/percolate_core/content/__init__.py +5 -0
  40. percolate_core-0.1.0/percolate_core/content/server.py +115 -0
  41. percolate_core-0.1.0/percolate_core/content/storage.py +63 -0
  42. percolate_core-0.1.0/percolate_core/core/__init__.py +14 -0
  43. percolate_core-0.1.0/percolate_core/core/config.py +48 -0
  44. percolate_core-0.1.0/percolate_core/core/identity.py +128 -0
  45. percolate_core-0.1.0/percolate_core/worker/__init__.py +8 -0
  46. percolate_core-0.1.0/percolate_core/worker/http.py +100 -0
  47. percolate_core-0.1.0/percolate_core/worker/loop.py +111 -0
  48. percolate_core-0.1.0/percolate_core/worker/templates.py +84 -0
  49. percolate_core-0.1.0/pyproject.toml +52 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Percolate
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,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: percolate-core
3
+ Version: 0.1.0
4
+ Summary: Percolate core: the worker, Content Server and Agent Runtime for a Postgres-native stack
5
+ Project-URL: Homepage, https://github.com/percolating-sirsh/percolate-core
6
+ Project-URL: Repository, https://github.com/percolating-sirsh/percolate-core
7
+ Project-URL: Specs, https://github.com/percolating-sirsh/p8-subsystems
8
+ Author: Percolate
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,pgvector,postgres,rag,workflow
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: asyncpg>=0.30
20
+ Requires-Dist: httpx>=0.28
21
+ Requires-Dist: typer>=0.15
22
+ Provides-Extra: agent
23
+ Requires-Dist: fastapi>=0.115; extra == 'agent'
24
+ Requires-Dist: mcp>=1.2; extra == 'agent'
25
+ Requires-Dist: pydantic-ai-slim[openai]>=0.4; extra == 'agent'
26
+ Requires-Dist: pydantic>=2.9; extra == 'agent'
27
+ Requires-Dist: pyyaml>=6.0; extra == 'agent'
28
+ Requires-Dist: rich>=13.9; extra == 'agent'
29
+ Requires-Dist: uvicorn>=0.34; extra == 'agent'
30
+ Provides-Extra: all
31
+ Requires-Dist: boto3>=1.35; extra == 'all'
32
+ Requires-Dist: fastapi>=0.115; extra == 'all'
33
+ Requires-Dist: mcp>=1.2; extra == 'all'
34
+ Requires-Dist: pydantic-ai-slim[openai]>=0.4; extra == 'all'
35
+ Requires-Dist: pydantic>=2.9; extra == 'all'
36
+ Requires-Dist: pyyaml>=6.0; extra == 'all'
37
+ Requires-Dist: rich>=13.9; extra == 'all'
38
+ Requires-Dist: uvicorn>=0.34; extra == 'all'
39
+ Provides-Extra: content
40
+ Requires-Dist: boto3>=1.35; extra == 'content'
41
+ Requires-Dist: fastapi>=0.115; extra == 'content'
42
+ Requires-Dist: uvicorn>=0.34; extra == 'content'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # p8
46
+
47
+ The processes that sit in front of the database. `../../INSTALL.md` covers the
48
+ database itself; nothing here is required to use it.
49
+
50
+ ```bash
51
+ pip install p8 # the worker -- asyncpg, httpx, typer
52
+ pip install 'p8[content]' # + Content Server (boto3, fastapi)
53
+ pip install 'p8[agent]' # + Agent Runtime (pydantic-ai, mcp)
54
+ pip install 'p8[all]'
55
+ ```
56
+
57
+ ```bash
58
+ p8 worker --queue http # claim and execute tasks
59
+ p8 content serve # uploads, scraping, http_call execution
60
+ p8 agent serve # agents, streaming, delegation
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Layout
66
+
67
+ | Module | Extra | What it is |
68
+ |---|---|---|
69
+ | `p8.core` | — | connecting **as the caller**, configuration, credential resolution |
70
+ | `p8.worker` | — | the step loop and the `@handler` registry |
71
+ | `p8.content` | `content` | the Content Server |
72
+ | `p8.agentic` | `agent` | the Agent Runtime |
73
+
74
+ **One distribution rather than three.** Three packages means a version matrix
75
+ (`p8content 0.3` requiring `p8core >=0.2,<0.3`) resolved forever, for services
76
+ that release together and are written by the same people. Splitting later is
77
+ mechanical; merging two that have drifted is not.
78
+
79
+ **The base install stays small on purpose.** The common case is someone writing
80
+ their own worker, and they should not pull boto3 and pydantic-ai to do it. The
81
+ CLI imports each subpackage lazily, so `p8 worker` runs without either
82
+ installed and `p8 content serve` fails with the extra to install rather than an
83
+ `ImportError`.
84
+
85
+ ---
86
+
87
+ ## `p8.core` — the one that matters
88
+
89
+ It decides **whose** RLS applies, and it exists because that logic was
90
+ previously written three times on two different database drivers.
91
+
92
+ ```python
93
+ from p8.core import as_caller
94
+
95
+ async with as_caller(claims) as conn: # claims = the VERIFIED JWT payload
96
+ rows = await conn.fetch("select * from content.resources")
97
+ ```
98
+
99
+ Every service connects as a low-privilege role and sets the caller's claims
100
+ **per transaction**, exactly as PostgREST does. A service that queried as
101
+ *itself* would bypass every policy in the collection — not by exploiting
102
+ anything, just by never presenting an identity for the policies to filter on.
103
+
104
+ Transaction-local (`set_config(..., true)`) is not a detail: an unregistered
105
+ GUC left at session scope survives into the next transaction on a pooled
106
+ connection, so the following request would inherit the previous caller's
107
+ identity.
108
+
109
+ `as_service()` exists for work with genuinely no user behind it — a scheduled
110
+ poll, a reconciliation sweep. Deliberately a separate function rather than
111
+ `as_caller(None)`, so "this query has no user" is something someone wrote down.
112
+
113
+ ---
114
+
115
+ ## Writing your own worker
116
+
117
+ Most steps need no worker from you:
118
+
119
+ | kind | who runs it |
120
+ |---|---|
121
+ | `sql` / `p8ql` | **nobody** — executes inside Postgres |
122
+ | `http_call` | the built-in handler |
123
+ | `timer` / `signal` / `decision` / `sub_workflow` | the engine |
124
+ | `work` | **you** |
125
+
126
+ When you do need one, it is this loop with a handler registered — not a
127
+ different program:
128
+
129
+ ```python
130
+ from p8.worker import handler, run
131
+
132
+ @handler("transcode")
133
+ async def transcode(spec, ctx):
134
+ return {"duration": await ffmpeg(ctx["run_input"]["file_key"])}
135
+
136
+ run(queue="media")
137
+ ```
138
+
139
+ `ctx` comes from `workflow.get_task_context()`: `run_input`, the accumulated
140
+ `context` (so a later step reads an earlier step's output), `task_input`,
141
+ `step_key`, and `trace_id`/`span_id`.
142
+
143
+ **The worker holds no table grants.** Every interaction is a `SECURITY DEFINER`
144
+ function call — `claim_task`, `get_task_context`, `complete_task`, `fail_task`
145
+ — which is why "bring your own worker" is safe to offer: a compromised worker
146
+ can claim and complete tasks, and nothing else.
147
+
148
+ **Raise `TerminalError` for what will not get better.** A bad argument, a
149
+ missing credential, a 404. Anything else is retried with backoff. The worker is
150
+ the only thing that knows what a failure means, so it decides and the engine
151
+ honours the verdict.
152
+
153
+ ---
154
+
155
+ ## Configuration
156
+
157
+ Environment only. Credentials by **reference**, never by value:
158
+ `credential_ref: "LLM_API_KEY"` on a task names a variable this process
159
+ resolves, so `workflow.tasks` stays inspectable and replayable.
160
+
161
+ | | Used by |
162
+ |---|---|
163
+ | `P8_DSN` | all |
164
+ | `P8_JWT_SECRET` | services verifying bearer tokens (same secret PostgREST uses) |
165
+ | `P8_QUEUE`, `P8_WORKER_ID`, `P8_POLL_SECONDS` | worker |
166
+ | `P8_S3_ENDPOINT`, `P8_S3_KEY`, `P8_S3_SECRET`, `P8_BUCKET` | content |
167
+
168
+ ---
169
+
170
+ ## Deployment
171
+
172
+ One image, three entrypoints — they share `p8.core`, so three images would be
173
+ three builds of the same base and three tags to keep in step.
174
+
175
+ ```yaml
176
+ content: { image: p8/runtime:0.1, command: ["p8","content","serve"] }
177
+ agent: { image: p8/runtime:0.1, command: ["p8","agent","serve"] }
178
+ worker: { image: p8/runtime:0.1, command: ["p8","worker","--queue","http"] }
179
+ ```
180
+
181
+ See [`../../PACKAGING.md`](../../PACKAGING.md) for the reasoning and the
182
+ remaining work.
183
+
184
+ ---
185
+
186
+ ## Status
187
+
188
+ - `p8.core`, `p8.worker`, `p8.content` — built, and exercised against a live
189
+ PG19 instance with MinIO.
190
+ - `p8.agentic` — moved from `experiments/agentic-runtime` unchanged (it uses
191
+ relative imports throughout, so the move needed no edits). Its own tests have
192
+ not been re-run under the new namespace.
193
+ - No Dockerfile yet. No service-surface entries in `../../surface.sql`, which
194
+ by `meta/skills/spec-driven-development` §7 should exist **before** the
195
+ endpoints they describe.
@@ -0,0 +1,151 @@
1
+ # p8
2
+
3
+ The processes that sit in front of the database. `../../INSTALL.md` covers the
4
+ database itself; nothing here is required to use it.
5
+
6
+ ```bash
7
+ pip install p8 # the worker -- asyncpg, httpx, typer
8
+ pip install 'p8[content]' # + Content Server (boto3, fastapi)
9
+ pip install 'p8[agent]' # + Agent Runtime (pydantic-ai, mcp)
10
+ pip install 'p8[all]'
11
+ ```
12
+
13
+ ```bash
14
+ p8 worker --queue http # claim and execute tasks
15
+ p8 content serve # uploads, scraping, http_call execution
16
+ p8 agent serve # agents, streaming, delegation
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Layout
22
+
23
+ | Module | Extra | What it is |
24
+ |---|---|---|
25
+ | `p8.core` | — | connecting **as the caller**, configuration, credential resolution |
26
+ | `p8.worker` | — | the step loop and the `@handler` registry |
27
+ | `p8.content` | `content` | the Content Server |
28
+ | `p8.agentic` | `agent` | the Agent Runtime |
29
+
30
+ **One distribution rather than three.** Three packages means a version matrix
31
+ (`p8content 0.3` requiring `p8core >=0.2,<0.3`) resolved forever, for services
32
+ that release together and are written by the same people. Splitting later is
33
+ mechanical; merging two that have drifted is not.
34
+
35
+ **The base install stays small on purpose.** The common case is someone writing
36
+ their own worker, and they should not pull boto3 and pydantic-ai to do it. The
37
+ CLI imports each subpackage lazily, so `p8 worker` runs without either
38
+ installed and `p8 content serve` fails with the extra to install rather than an
39
+ `ImportError`.
40
+
41
+ ---
42
+
43
+ ## `p8.core` — the one that matters
44
+
45
+ It decides **whose** RLS applies, and it exists because that logic was
46
+ previously written three times on two different database drivers.
47
+
48
+ ```python
49
+ from p8.core import as_caller
50
+
51
+ async with as_caller(claims) as conn: # claims = the VERIFIED JWT payload
52
+ rows = await conn.fetch("select * from content.resources")
53
+ ```
54
+
55
+ Every service connects as a low-privilege role and sets the caller's claims
56
+ **per transaction**, exactly as PostgREST does. A service that queried as
57
+ *itself* would bypass every policy in the collection — not by exploiting
58
+ anything, just by never presenting an identity for the policies to filter on.
59
+
60
+ Transaction-local (`set_config(..., true)`) is not a detail: an unregistered
61
+ GUC left at session scope survives into the next transaction on a pooled
62
+ connection, so the following request would inherit the previous caller's
63
+ identity.
64
+
65
+ `as_service()` exists for work with genuinely no user behind it — a scheduled
66
+ poll, a reconciliation sweep. Deliberately a separate function rather than
67
+ `as_caller(None)`, so "this query has no user" is something someone wrote down.
68
+
69
+ ---
70
+
71
+ ## Writing your own worker
72
+
73
+ Most steps need no worker from you:
74
+
75
+ | kind | who runs it |
76
+ |---|---|
77
+ | `sql` / `p8ql` | **nobody** — executes inside Postgres |
78
+ | `http_call` | the built-in handler |
79
+ | `timer` / `signal` / `decision` / `sub_workflow` | the engine |
80
+ | `work` | **you** |
81
+
82
+ When you do need one, it is this loop with a handler registered — not a
83
+ different program:
84
+
85
+ ```python
86
+ from p8.worker import handler, run
87
+
88
+ @handler("transcode")
89
+ async def transcode(spec, ctx):
90
+ return {"duration": await ffmpeg(ctx["run_input"]["file_key"])}
91
+
92
+ run(queue="media")
93
+ ```
94
+
95
+ `ctx` comes from `workflow.get_task_context()`: `run_input`, the accumulated
96
+ `context` (so a later step reads an earlier step's output), `task_input`,
97
+ `step_key`, and `trace_id`/`span_id`.
98
+
99
+ **The worker holds no table grants.** Every interaction is a `SECURITY DEFINER`
100
+ function call — `claim_task`, `get_task_context`, `complete_task`, `fail_task`
101
+ — which is why "bring your own worker" is safe to offer: a compromised worker
102
+ can claim and complete tasks, and nothing else.
103
+
104
+ **Raise `TerminalError` for what will not get better.** A bad argument, a
105
+ missing credential, a 404. Anything else is retried with backoff. The worker is
106
+ the only thing that knows what a failure means, so it decides and the engine
107
+ honours the verdict.
108
+
109
+ ---
110
+
111
+ ## Configuration
112
+
113
+ Environment only. Credentials by **reference**, never by value:
114
+ `credential_ref: "LLM_API_KEY"` on a task names a variable this process
115
+ resolves, so `workflow.tasks` stays inspectable and replayable.
116
+
117
+ | | Used by |
118
+ |---|---|
119
+ | `P8_DSN` | all |
120
+ | `P8_JWT_SECRET` | services verifying bearer tokens (same secret PostgREST uses) |
121
+ | `P8_QUEUE`, `P8_WORKER_ID`, `P8_POLL_SECONDS` | worker |
122
+ | `P8_S3_ENDPOINT`, `P8_S3_KEY`, `P8_S3_SECRET`, `P8_BUCKET` | content |
123
+
124
+ ---
125
+
126
+ ## Deployment
127
+
128
+ One image, three entrypoints — they share `p8.core`, so three images would be
129
+ three builds of the same base and three tags to keep in step.
130
+
131
+ ```yaml
132
+ content: { image: p8/runtime:0.1, command: ["p8","content","serve"] }
133
+ agent: { image: p8/runtime:0.1, command: ["p8","agent","serve"] }
134
+ worker: { image: p8/runtime:0.1, command: ["p8","worker","--queue","http"] }
135
+ ```
136
+
137
+ See [`../../PACKAGING.md`](../../PACKAGING.md) for the reasoning and the
138
+ remaining work.
139
+
140
+ ---
141
+
142
+ ## Status
143
+
144
+ - `p8.core`, `p8.worker`, `p8.content` — built, and exercised against a live
145
+ PG19 instance with MinIO.
146
+ - `p8.agentic` — moved from `experiments/agentic-runtime` unchanged (it uses
147
+ relative imports throughout, so the move needed no edits). Its own tests have
148
+ not been re-run under the new namespace.
149
+ - No Dockerfile yet. No service-surface entries in `../../surface.sql`, which
150
+ by `meta/skills/spec-driven-development` §7 should exist **before** the
151
+ endpoints they describe.
@@ -0,0 +1,13 @@
1
+ """percolate-core — Postgres-native subsystems.
2
+
3
+ The database is the system; these are the processes that sit in front of it.
4
+ Everything here connects through the same RLS-scoped roles and holds no table
5
+ grants: every interaction is a SECURITY DEFINER function call.
6
+
7
+ percolate_core.core connecting as the caller, configuration (always)
8
+ percolate_core.worker the step loop and @handler registry (always)
9
+ percolate_core.content the Content Server [content]
10
+ percolate_core.agentic the Agent Runtime [agent]
11
+ """
12
+
13
+ __version__ = "0.1.0"
@@ -0,0 +1,48 @@
1
+ """p8agentic — the agent runtime speced in ``specs/agentic``.
2
+
3
+ Agents are rows in Postgres, tools are MCP/OpenAPI services reached over the
4
+ network, a turn's rows are written through RLS as an ordinary ``authenticated``
5
+ caller, AG-UI events stream out over ``NOTIFY``, and delegation is an MCP call
6
+ into this runtime's own agent gateway. ``dev/verify.py`` asserts all of that
7
+ against a live LLM.
8
+
9
+ Layout, mapped onto ``specs/agentic/brief.md``:
10
+
11
+ - ``contracts`` — the ontology as pydantic types (brief §7, ontology.md), plus
12
+ the two wire protocols (brief §5.1 item 2).
13
+ - ``db`` — a small asyncpg client and the ``Repository`` protocol every
14
+ read/write in this runtime goes through (brief §5: "through the same functions
15
+ and RLS any other client would go through, no privileged backdoor").
16
+ - ``tools`` — tool-server resolution and invocation: MCP is the canonical
17
+ shape, OpenAPI is an adapter onto it (brief §5.2). Zero built-in tools.
18
+ - ``runtime`` — the four components of brief §5.1, one module each, plus the
19
+ chained action (§4) and citation (§5.3) mechanisms.
20
+ - ``sequencer`` — the ordered steps of one agent turn, and the mapping from
21
+ ``specs/agentic/sequence.md``'s phase gates onto demos that prove them.
22
+ - ``fakes`` — pydantic-ai fake models and an in-memory repository, so the
23
+ whole path above runs with no Postgres and no LLM credentials.
24
+ - ``cli`` — the operator surface. FastAPI wraps this later (brief §4:
25
+ streaming is the one piece that structurally can't be plain PostgREST).
26
+ """
27
+
28
+ __version__ = "0.0.1"
29
+
30
+
31
+ def build_fingerprint() -> str:
32
+ """A short hash of this package's source on disk.
33
+
34
+ Exposed by every long-running surface (`/_build`) so a caller can tell
35
+ whether the process it is talking to is running the code in the working
36
+ tree. Three separate debugging sessions in this module's history began with
37
+ a confusing failure whose cause was a server started before the fix — a
38
+ stale process fails in the shape of a bug, and that is worth fifteen lines
39
+ to make impossible.
40
+ """
41
+ import hashlib
42
+ from pathlib import Path
43
+
44
+ digest = hashlib.sha256()
45
+ for path in sorted(Path(__file__).parent.rglob("*.py")):
46
+ digest.update(path.name.encode())
47
+ digest.update(str(path.stat().st_mtime_ns).encode())
48
+ return digest.hexdigest()[:12]