jasil 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.
- jasil-0.1.0/.gitignore +32 -0
- jasil-0.1.0/CHANGELOG.md +187 -0
- jasil-0.1.0/LICENSE.md +7 -0
- jasil-0.1.0/PKG-INFO +221 -0
- jasil-0.1.0/README.md +171 -0
- jasil-0.1.0/SECURITY.md +49 -0
- jasil-0.1.0/jasil/__init__.py +48 -0
- jasil-0.1.0/jasil/_core/__init__.py +5 -0
- jasil-0.1.0/jasil/_core/dialects.py +43 -0
- jasil-0.1.0/jasil/_core/identity.py +34 -0
- jasil-0.1.0/jasil/_core/limits.py +95 -0
- jasil-0.1.0/jasil/_core/network.py +199 -0
- jasil-0.1.0/jasil/_core/optional_deps.py +30 -0
- jasil-0.1.0/jasil/_core/pruning.py +58 -0
- jasil-0.1.0/jasil/_core/redis_clients.py +219 -0
- jasil-0.1.0/jasil/_core/registry.py +75 -0
- jasil-0.1.0/jasil/_core/sessions.py +27 -0
- jasil-0.1.0/jasil/_core/storage_keys.py +38 -0
- jasil-0.1.0/jasil/_core/threads.py +44 -0
- jasil-0.1.0/jasil/_core/timestamps.py +48 -0
- jasil-0.1.0/jasil/admin.py +124 -0
- jasil-0.1.0/jasil/backends/__init__.py +10 -0
- jasil-0.1.0/jasil/backends/clock_system.py +14 -0
- jasil-0.1.0/jasil/backends/events_inprocess.py +56 -0
- jasil-0.1.0/jasil/backends/events_redis.py +186 -0
- jasil-0.1.0/jasil/backends/geocoding_http.py +237 -0
- jasil-0.1.0/jasil/backends/lock_noop.py +17 -0
- jasil-0.1.0/jasil/backends/lock_pg.py +77 -0
- jasil-0.1.0/jasil/backends/state_memory.py +129 -0
- jasil-0.1.0/jasil/backends/state_redis.py +165 -0
- jasil-0.1.0/jasil/backends/storage_local.py +109 -0
- jasil-0.1.0/jasil/backends/storage_s3.py +120 -0
- jasil-0.1.0/jasil/capabilities.py +309 -0
- jasil-0.1.0/jasil/container.py +285 -0
- jasil-0.1.0/jasil/correlation.py +75 -0
- jasil-0.1.0/jasil/deps.py +67 -0
- jasil-0.1.0/jasil/event_log/__init__.py +1 -0
- jasil-0.1.0/jasil/event_log/crud.py +336 -0
- jasil-0.1.0/jasil/event_log/models.py +131 -0
- jasil-0.1.0/jasil/event_log/recorder.py +115 -0
- jasil-0.1.0/jasil/event_log/schema.py +101 -0
- jasil-0.1.0/jasil/event_versioning.py +154 -0
- jasil-0.1.0/jasil/events.py +148 -0
- jasil-0.1.0/jasil/jobs/__init__.py +10 -0
- jasil-0.1.0/jasil/jobs/backoff.py +51 -0
- jasil-0.1.0/jasil/jobs/crud.py +529 -0
- jasil-0.1.0/jasil/jobs/models.py +267 -0
- jasil-0.1.0/jasil/jobs/outbox.py +133 -0
- jasil-0.1.0/jasil/jobs/reconciliation.py +115 -0
- jasil-0.1.0/jasil/jobs/registry.py +95 -0
- jasil-0.1.0/jasil/jobs/relay.py +100 -0
- jasil-0.1.0/jasil/jobs/runner.py +185 -0
- jasil-0.1.0/jasil/jobs/schema.py +99 -0
- jasil-0.1.0/jasil/jobs/service.py +166 -0
- jasil-0.1.0/jasil/jobs/worker.py +89 -0
- jasil-0.1.0/jasil/lifecycle.py +72 -0
- jasil-0.1.0/jasil/migrations/__init__.py +156 -0
- jasil-0.1.0/jasil/migrations/env.py +64 -0
- jasil-0.1.0/jasil/migrations/script.py.mako +24 -0
- jasil-0.1.0/jasil/migrations/versions/rev0001_initial.py +242 -0
- jasil-0.1.0/jasil/orm.py +224 -0
- jasil-0.1.0/jasil/profile.py +135 -0
- jasil-0.1.0/jasil/providers.py +197 -0
- jasil-0.1.0/jasil/publisher.py +263 -0
- jasil-0.1.0/jasil/py.typed +0 -0
- jasil-0.1.0/jasil/retention.py +148 -0
- jasil-0.1.0/jasil/runtime.py +74 -0
- jasil-0.1.0/jasil/settings.py +247 -0
- jasil-0.1.0/jasil/subscribers.py +60 -0
- jasil-0.1.0/jasil/testing.py +131 -0
- jasil-0.1.0/pyproject.toml +436 -0
jasil-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# OS
|
|
2
|
+
.DS_Store
|
|
3
|
+
Thumbs.db
|
|
4
|
+
.vscode/
|
|
5
|
+
.env
|
|
6
|
+
.venv/
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
__pycache__/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.hypothesis/
|
|
12
|
+
.coverage
|
|
13
|
+
coverage.json
|
|
14
|
+
htmlcov/
|
|
15
|
+
coverage.xml
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
*.log
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.import_linter_cache/
|
|
20
|
+
|
|
21
|
+
# local dev docs
|
|
22
|
+
devdocs/
|
|
23
|
+
|
|
24
|
+
# local dev scripts
|
|
25
|
+
devscripts/
|
|
26
|
+
|
|
27
|
+
# built documentation site (mkdocs)
|
|
28
|
+
site/
|
|
29
|
+
|
|
30
|
+
# build artifacts
|
|
31
|
+
dist/
|
|
32
|
+
*.egg-info/
|
jasil-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to JASIL are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
|
+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
What counts as a breaking change — and what does not — is defined in
|
|
9
|
+
[API stability](docs/api-stability.md). In short: the `jasil` namespace, the
|
|
10
|
+
capability provider protocols, the event envelope, the capability URI schemes,
|
|
11
|
+
and the database schema of JASIL's own tables are covered by SemVer; anything
|
|
12
|
+
under `jasil._core`, log message text, and the wording of error messages are not.
|
|
13
|
+
|
|
14
|
+
## [0.1.0]
|
|
15
|
+
|
|
16
|
+
First release. Extracted from [Endurain](https://github.com/endurain-project/endurain),
|
|
17
|
+
where this code ran in production as an internal `infra` package. The API is
|
|
18
|
+
still settling: `0.x` releases may break it, and the SemVer guarantees in
|
|
19
|
+
[API stability](docs/api-stability.md) begin at `1.0.0`.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
**Capability providers and backends**
|
|
24
|
+
|
|
25
|
+
- `StateProvider` — ephemeral keyed state with TTLs, backed by process-local
|
|
26
|
+
memory (`memory://`) or Redis (`redis://` / `rediss://` / `unix://`). Beyond
|
|
27
|
+
plain key/value access it exposes the atomic primitives correctness actually
|
|
28
|
+
depends on — `set_if_absent`, `get_and_delete`, and `record_tiered_failure`
|
|
29
|
+
(an atomic tiered lockout, implemented under a lock in memory and as a Lua
|
|
30
|
+
script on Redis) — so a store behaves identically on either backend.
|
|
31
|
+
- `StorageProvider` — opaque blob storage addressed by `(area, key)`, backed by
|
|
32
|
+
the local filesystem (`local://`) or S3-compatible object storage (`s3://`).
|
|
33
|
+
Both backends refuse the same addresses — empty, absolute, or containing a
|
|
34
|
+
`..` component — before touching a disk or a client, so the contract a caller
|
|
35
|
+
sees does not change with the deployment. `list_keys` is recursive on both, so
|
|
36
|
+
a nested key is listed wherever it is stored.
|
|
37
|
+
- `EventBusProvider` — synchronous in-process dispatch (`memory://`) or Redis
|
|
38
|
+
Streams with a consumer group (`redis://`), giving competing-consumer
|
|
39
|
+
semantics across replicas.
|
|
40
|
+
- `LockProvider` — a no-op lock (`noop://`) or PostgreSQL session-level advisory
|
|
41
|
+
locks (`postgres-advisory://`), which need no infrastructure beyond the
|
|
42
|
+
database the host already has.
|
|
43
|
+
- `ClockProvider` and `GeocodingProvider`, the latter covering Nominatim,
|
|
44
|
+
Photon, and geocode.maps.co behind one interface.
|
|
45
|
+
- A Redis outage surfaces to callers as `StateBackendUnavailableError`, so
|
|
46
|
+
domain code never imports or catches a redis-py exception.
|
|
47
|
+
|
|
48
|
+
**Deployment profiles**
|
|
49
|
+
|
|
50
|
+
- `local`, `distributed`, and `custom` profiles. The profile supplies the
|
|
51
|
+
default for each capability URI, and the `local` profile is the zero-config
|
|
52
|
+
default: memory state, local disk, in-process events, no-op lock.
|
|
53
|
+
- `distributed` and `custom` **refuse to start** when a capability URI is unset
|
|
54
|
+
rather than falling back to a process-local backend, which across replicas
|
|
55
|
+
would diverge silently — the failure the profile system exists to prevent.
|
|
56
|
+
- A startup capability report showing how each capability resolved and why, plus
|
|
57
|
+
consistency checks that reject a multi-process topology wired to
|
|
58
|
+
process-local state. `build_platform()` runs both before it constructs a single
|
|
59
|
+
backend; set `enforce_deployment_consistency=False` to downgrade a refusal to a
|
|
60
|
+
warning.
|
|
61
|
+
|
|
62
|
+
**Events**
|
|
63
|
+
|
|
64
|
+
- One immutable `Event` envelope with an ISO-8601 UTC timestamp, a UUIDv4 id
|
|
65
|
+
stable across retries, and a caller-owned `metadata` dict.
|
|
66
|
+
- `event_id`, `event_type` and `source` are length-checked when the envelope is
|
|
67
|
+
minted, and `subscriber_id` when a durable subscriber registers, so a value
|
|
68
|
+
too long for the column it is persisted in raises at the producing call site
|
|
69
|
+
instead of failing at the write — where PostgreSQL and MySQL raise, SQLite
|
|
70
|
+
does not, and the publish seam swallows the failure either way. `payload` and
|
|
71
|
+
`metadata` are deliberately *not* capped; see the events documentation for what
|
|
72
|
+
belongs in them.
|
|
73
|
+
- Payload schema versioning: `VersionedPayload` carries a `SCHEMA_VERSION` and
|
|
74
|
+
per-step `UPGRADERS`. A payload written by an older build is walked forward
|
|
75
|
+
one version at a time; one written by a *newer* build is refused rather than
|
|
76
|
+
silently misread — the failure mode during a rolling deploy.
|
|
77
|
+
- `publisher.publish` is the single publish seam. Delivery failures are logged
|
|
78
|
+
and swallowed so a publish never breaks the producer.
|
|
79
|
+
- `publish_committing` / `publish_many_committing` own the commit ordering, so
|
|
80
|
+
durable delivery can be made atomic with the caller's domain write.
|
|
81
|
+
- `subscribers.best_effort` wraps a raising handler into a swallowing bus
|
|
82
|
+
subscriber, so derived work can never fail the request that produced the event.
|
|
83
|
+
|
|
84
|
+
**Durable jobs**
|
|
85
|
+
|
|
86
|
+
- A transactional outbox relayed into leased per-subscriber jobs, with
|
|
87
|
+
exponential backoff (equal jitter, to avoid a retry stampede), an attempt
|
|
88
|
+
ceiling, and a dead-letter queue with replay.
|
|
89
|
+
- Idempotent consumers: `(event_id, subscriber_id)` uniqueness is enforced by
|
|
90
|
+
the database, so a re-delivered event never runs a subscriber twice.
|
|
91
|
+
- Lease reclamation returns work stranded by a crashed worker; an attempt is
|
|
92
|
+
counted at claim time, which is what bounds a crash loop.
|
|
93
|
+
- A worker's identity is bounded to the width of the lease column it is written
|
|
94
|
+
to. When a long hostname forces truncation it carries a digest of the full
|
|
95
|
+
value, so two machines sharing a hostname prefix never collapse onto one lease
|
|
96
|
+
holder — which would hand each of them the other's claimed rows.
|
|
97
|
+
- On PostgreSQL, claiming and relaying use `FOR UPDATE SKIP LOCKED` so
|
|
98
|
+
concurrent workers and relayers take disjoint batches with no coordinating lock.
|
|
99
|
+
- `DurableSubscriberNet` declares the reconciliation net — a scheduled backfill,
|
|
100
|
+
or a documented exemption — that every subscriber writing durable derived state
|
|
101
|
+
owes, since delivery is at-least-once but not guaranteed. `assert_nets_complete`
|
|
102
|
+
holds the whole registry to it, so a missing net fails your own test suite
|
|
103
|
+
rather than surfacing in production.
|
|
104
|
+
|
|
105
|
+
**Observability**
|
|
106
|
+
|
|
107
|
+
- An `event_log` table recording each event's lifecycle, written by the bus and
|
|
108
|
+
the publish facade, with dashboard aggregates.
|
|
109
|
+
- Identifiers that are too long for the column they are persisted in are refused
|
|
110
|
+
at the producing call site; derived, diagnostic values (failure text, the
|
|
111
|
+
joined subscriber list, a worker identity) are truncated with a marker instead,
|
|
112
|
+
so a reader can tell the value was cut.
|
|
113
|
+
- Retention pruning in bounded batches, on independently configurable windows.
|
|
114
|
+
In-flight rows and dead-letters are never pruned. Register it with
|
|
115
|
+
`jasil.retention.schedule_retention_maintenance(scheduler)`, the counterpart of
|
|
116
|
+
`jasil.jobs.service.schedule_job_maintenance` — separate because retention also
|
|
117
|
+
covers the `event_log`, so it applies without durable jobs.
|
|
118
|
+
|
|
119
|
+
**Host integration**
|
|
120
|
+
|
|
121
|
+
- The **host** owns the declarative base and the engine. JASIL maps its tables
|
|
122
|
+
into the host's registry via `map_models(Base)` and takes a session factory via
|
|
123
|
+
`configure_sessionmaker(...)`. JASIL never creates an engine.
|
|
124
|
+
`map_models` must run before `jasil.jobs.crud` or `jasil.event_log.crud` is
|
|
125
|
+
imported; every other public module — `jasil.publisher` above all, which every
|
|
126
|
+
producer imports at module scope — defers its model imports and is safe to
|
|
127
|
+
import from anywhere in the host's import graph.
|
|
128
|
+
- `jasil.settings` — immutable, grouped configuration installed by the host.
|
|
129
|
+
JASIL reads no environment variables and no secret files.
|
|
130
|
+
- `jasil.correlation` — a pluggable correlation-id provider, defaulting to a
|
|
131
|
+
module-local context variable, so events carry the host's request id.
|
|
132
|
+
- Packaged Alembic revisions (`jasil[migrations]`) on their own version table,
|
|
133
|
+
`jasil_alembic_version`, scoped to JASIL's tables so the host's Alembic
|
|
134
|
+
history and tables are never touched.
|
|
135
|
+
- FastAPI dependency helpers behind the `fastapi` extra, resolving
|
|
136
|
+
`app.state.platform` when the host attached one and otherwise the process-wide
|
|
137
|
+
platform, so the quick-start wiring needs nothing extra.
|
|
138
|
+
- `jasil.admin` — the operator-facing surface: `get_jobs_summary`,
|
|
139
|
+
`get_event_log_summary`, and `replay_dead_letter_job`, plus the response
|
|
140
|
+
schemas. Importable from anywhere in the host's import graph and takes no
|
|
141
|
+
session, so an admin route cannot hand JASIL its own open transaction. The
|
|
142
|
+
CRUD modules behind it stay internal.
|
|
143
|
+
- `jasil.testing` — `FixedClock`, `install_test_platform`, and `reset_all`, so a
|
|
144
|
+
host's suite does not have to rediscover which process-wide slots JASIL
|
|
145
|
+
installs. `reset_all` deliberately leaves the ORM mapping in place; model
|
|
146
|
+
modules capture the declarative base at import time.
|
|
147
|
+
- `Platform.close()` releases what the platform owns — the event-bus consumer
|
|
148
|
+
thread and the shared Redis clients — and never raises, so a shutdown failure
|
|
149
|
+
cannot mask whatever prompted the shutdown. The durable-job worker and the
|
|
150
|
+
host's engine are stopped separately, because the platform does not own them.
|
|
151
|
+
- `jasil.lifecycle.shutdown()` composes the two halves of that in the order that
|
|
152
|
+
matters — the worker stops before the bus its subscribers publish through — so
|
|
153
|
+
a host does not have to know the ordering. Idempotent, safe before anything has
|
|
154
|
+
started, and never raises.
|
|
155
|
+
|
|
156
|
+
**Packaging**
|
|
157
|
+
|
|
158
|
+
- A core install requires only `sqlalchemy` and `pydantic`. Every backend client
|
|
159
|
+
lives behind an extra (`redis`, `s3`, `postgres`, `jobs`, `fastapi`,
|
|
160
|
+
`geocoding`, `migrations`, `all`) and is imported lazily, so a single-process
|
|
161
|
+
deployment loads none of them.
|
|
162
|
+
- Ships `py.typed`.
|
|
163
|
+
|
|
164
|
+
### Security
|
|
165
|
+
|
|
166
|
+
- SSRF guard on every outbound host JASIL dials on the host's behalf. All
|
|
167
|
+
resolved addresses must be public unicast — a single private, loopback,
|
|
168
|
+
link-local, or reserved answer rejects the host, which is what defends against
|
|
169
|
+
DNS rebinding. An allowlist escape hatch exists for self-hosted services on
|
|
170
|
+
private networks, and every use of it is logged for audit. Configured values
|
|
171
|
+
must be a bare `host[:port]`, so one carrying a scheme or path cannot redirect
|
|
172
|
+
a request elsewhere. An allowlist entry that is a *hostname* exempts every
|
|
173
|
+
address that name resolves to, so taking one is logged at `WARNING` naming the
|
|
174
|
+
address and recommending a CIDR; a CIDR exemption logs at `INFO`.
|
|
175
|
+
- The geocoding backend refuses redirects, so a permitted host cannot 3xx-pivot
|
|
176
|
+
onto an internal target. Its response body is read under a size cap, and its
|
|
177
|
+
failures are logged by exception type and status code only: `requests` puts the
|
|
178
|
+
request URL in an error message, and that URL carries the API key.
|
|
179
|
+
- The Redis state backend escapes glob metacharacters before turning a caller's
|
|
180
|
+
key prefix into a `SCAN`/`MATCH` pattern, so a prefix holding `*`, `?` or
|
|
181
|
+
`[...]` matches only itself and never widens onto keys the caller did not name.
|
|
182
|
+
A prefix built from a tenant or user identifier is therefore safe.
|
|
183
|
+
- The local storage backend rejects absolute and parent-traversal area and key
|
|
184
|
+
values before any filesystem access, and percent-encodes both into the URL it
|
|
185
|
+
returns, so a key holding `?`, `#` or `%` cannot alter the URL it lands in.
|
|
186
|
+
|
|
187
|
+
[0.1.0]: https://github.com/endurain-project/jasil/releases/tag/v0.1.0
|
jasil-0.1.0/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2025 João Vitória Silva
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
jasil-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jasil
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Just Another Substrate & Infrastructure Library
|
|
5
|
+
Project-URL: Homepage, https://jasil.endurain.com/
|
|
6
|
+
Project-URL: Documentation, https://jasil.endurain.com/
|
|
7
|
+
Project-URL: Repository, https://github.com/endurain-project/jasil
|
|
8
|
+
Project-URL: Changelog, https://github.com/endurain-project/jasil/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/endurain-project/jasil/issues
|
|
10
|
+
Project-URL: Security, https://github.com/endurain-project/jasil/blob/main/SECURITY.md
|
|
11
|
+
Author-email: João Vitória Silva <joao@endurain.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE.md
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: <4.0,>=3.12
|
|
25
|
+
Requires-Dist: pydantic>=2.12.5
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0.49
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: alembic>=1.13; extra == 'all'
|
|
29
|
+
Requires-Dist: apscheduler>=3.10.2; extra == 'all'
|
|
30
|
+
Requires-Dist: boto3>=1.34.0; extra == 'all'
|
|
31
|
+
Requires-Dist: fastapi>=0.136.0; extra == 'all'
|
|
32
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'all'
|
|
33
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
34
|
+
Requires-Dist: requests>=2.32.4; extra == 'all'
|
|
35
|
+
Provides-Extra: fastapi
|
|
36
|
+
Requires-Dist: fastapi>=0.136.0; extra == 'fastapi'
|
|
37
|
+
Provides-Extra: geocoding
|
|
38
|
+
Requires-Dist: requests>=2.32.4; extra == 'geocoding'
|
|
39
|
+
Provides-Extra: jobs
|
|
40
|
+
Requires-Dist: apscheduler>=3.10.2; extra == 'jobs'
|
|
41
|
+
Provides-Extra: migrations
|
|
42
|
+
Requires-Dist: alembic>=1.13; extra == 'migrations'
|
|
43
|
+
Provides-Extra: postgres
|
|
44
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'postgres'
|
|
45
|
+
Provides-Extra: redis
|
|
46
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
47
|
+
Provides-Extra: s3
|
|
48
|
+
Requires-Dist: boto3>=1.34.0; extra == 's3'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# JASIL
|
|
52
|
+
|
|
53
|
+
[](https://github.com/endurain-project/jasil/blob/main/LICENSE.md)
|
|
54
|
+
[](https://github.com/endurain-project/jasil/releases)
|
|
55
|
+
[](https://pypi.org/project/jasil/)
|
|
56
|
+
[](https://pypi.org/project/jasil/)
|
|
57
|
+
[](https://pypi.org/project/jasil/)
|
|
58
|
+
[](https://jasil.endurain.com/)
|
|
59
|
+
[](https://github.com/endurain-project/jasil)
|
|
60
|
+
|
|
61
|
+
**J**ust **A**nother **S**ubstrate & **I**nfrastructure **L**ibrary — a
|
|
62
|
+
framework-agnostic infrastructure substrate for Python services: swappable
|
|
63
|
+
capability backends, an event pipeline, durable jobs, and observability.
|
|
64
|
+
|
|
65
|
+
> **Status:** `0.1.0` — the API is still settling. Expect breaking changes on
|
|
66
|
+
> minor versions until `1.0.0`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## What it gives you
|
|
71
|
+
|
|
72
|
+
| Layer | What it does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| **Providers** | Small protocols — state, storage, events, lock, clock, geocoding — that your domain code depends on instead of on Redis, S3, or Postgres. |
|
|
75
|
+
| **Backends** | A working implementation of each, selected by URI: `memory://` or `redis://`, `local://` or `s3://`, `noop://` or `postgres-advisory://`. |
|
|
76
|
+
| **Deployment profiles** | `local` runs single-process with zero extra infrastructure. `distributed` requires shared backends and refuses to guess them. |
|
|
77
|
+
| **Event pipeline** | One envelope, one publish seam, payload schema versioning that survives a rolling deploy. |
|
|
78
|
+
| **Durable jobs** | A transactional outbox relayed into leased per-subscriber jobs, with exponential backoff and a dead-letter queue. |
|
|
79
|
+
| **Observability** | An event lifecycle log and bounded retention pruning for every table it owns. |
|
|
80
|
+
|
|
81
|
+
A core install depends on **`sqlalchemy` and `pydantic` only**. Every backend
|
|
82
|
+
client lives behind an extra and is imported lazily, so a single-process
|
|
83
|
+
deployment never loads `redis`, `boto3`, or `requests`.
|
|
84
|
+
|
|
85
|
+
> **JASIL is synchronous.** Every provider, subscriber, and the durable-jobs
|
|
86
|
+
> layer is blocking Python, and the ORM integration expects a synchronous
|
|
87
|
+
> `sessionmaker` — there is no `AsyncSession` support. On an async framework,
|
|
88
|
+
> call into JASIL from a worker thread (a `def` FastAPI route or dependency does
|
|
89
|
+
> this for you). See [the docs](https://jasil.endurain.com/#one-thing-it-cannot-do-yet).
|
|
90
|
+
|
|
91
|
+
## Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install jasil # core: memory / local disk / in-process
|
|
95
|
+
pip install "jasil[redis,s3]" # distributed state, events and storage
|
|
96
|
+
pip install "jasil[all]" # every optional backend
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Extra | Enables |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `redis` | Redis state store and Redis-Streams event bus |
|
|
102
|
+
| `s3` | S3-compatible object storage |
|
|
103
|
+
| `postgres` | PostgreSQL advisory locks |
|
|
104
|
+
| `jobs` | Durable-job scheduling |
|
|
105
|
+
| `fastapi` | `Depends` helpers exposing the providers to routes |
|
|
106
|
+
| `geocoding` | HTTP reverse geocoding |
|
|
107
|
+
| `migrations` | Packaged Alembic revisions |
|
|
108
|
+
|
|
109
|
+
### Verifying a release
|
|
110
|
+
|
|
111
|
+
Releases are built and published by [this repository's release workflow](.github/workflows/publish-jasil.yml) through PyPI Trusted Publishing, with [PEP 740](https://peps.python.org/pep-0740/) attestations. You can confirm a downloaded artifact came from that workflow and was not substituted:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uvx pypi-attestations verify pypi \
|
|
115
|
+
--repository https://github.com/endurain-project/jasil \
|
|
116
|
+
pypi:jasil-<version>-py3-none-any.whl
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A successful run prints `OK: <filename>`. `Provenance for file ... was not found` means the artifact predates attested publishing rather than that verification failed.
|
|
120
|
+
|
|
121
|
+
Each release run also produces a CycloneDX SBOM and `SHA256SUMS`, generated from a clean install of the built wheel. These are retained as workflow artifacts on the release run rather than published to PyPI.
|
|
122
|
+
|
|
123
|
+
## Quick start
|
|
124
|
+
|
|
125
|
+
JASIL never reads your environment, creates your engine, or owns your
|
|
126
|
+
declarative base — the host supplies all three.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from sqlalchemy import create_engine
|
|
130
|
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
131
|
+
|
|
132
|
+
import jasil.orm as jasil_orm
|
|
133
|
+
import jasil.settings as jasil_settings
|
|
134
|
+
from jasil.container import build_platform
|
|
135
|
+
from jasil.runtime import set_active_platform
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Base(DeclarativeBase):
|
|
139
|
+
"""Your application's declarative base."""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# 1. Map JASIL's tables into your registry, before any database use.
|
|
143
|
+
jasil_orm.map_models(Base)
|
|
144
|
+
|
|
145
|
+
# 2. Hand JASIL a session factory bound to your engine.
|
|
146
|
+
engine = create_engine("postgresql+psycopg://...")
|
|
147
|
+
jasil_orm.configure_sessionmaker(sessionmaker(bind=engine))
|
|
148
|
+
|
|
149
|
+
# 3. Configure, build, publish.
|
|
150
|
+
jasil_settings.configure(jasil_settings.JasilSettings(data_dir="/srv/data"))
|
|
151
|
+
set_active_platform(build_platform())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then depend on capabilities, not on infrastructure:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from jasil.runtime import get_active_platform
|
|
158
|
+
|
|
159
|
+
platform = get_active_platform()
|
|
160
|
+
platform.state.set("session:abc", b"...", ttl_seconds=3600)
|
|
161
|
+
platform.storage.save("thumbnails", "42.webp", image_bytes)
|
|
162
|
+
|
|
163
|
+
with platform.lock.try_acquire("nightly-backfill") as acquired:
|
|
164
|
+
if acquired:
|
|
165
|
+
run_backfill()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Publishing an event goes through one seam, so switching from best-effort
|
|
169
|
+
delivery to a transactional outbox is a configuration change, not a rewrite:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from jasil.publisher import publish
|
|
173
|
+
|
|
174
|
+
publish("activity.created", {"activity_id": 42}, source="api:store_activity", db=db)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Going distributed
|
|
178
|
+
|
|
179
|
+
Change the profile and point the capability URIs at real infrastructure:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
jasil_settings.configure(
|
|
183
|
+
jasil_settings.JasilSettings(
|
|
184
|
+
profile=jasil.DeploymentProfile.DISTRIBUTED,
|
|
185
|
+
state_uri="redis://cache:6379/0",
|
|
186
|
+
events_uri="redis://cache:6379/1",
|
|
187
|
+
storage_uri="s3://my-bucket",
|
|
188
|
+
lock_uri="postgres-advisory://",
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `distributed` profile **refuses to start** if a capability URI is unset. A
|
|
194
|
+
silent fallback to a process-local backend across replicas is the failure the
|
|
195
|
+
profile system exists to prevent.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
uv sync --all-extras --group dev
|
|
201
|
+
uv run pytest # tests + coverage gate
|
|
202
|
+
uv run ruff check . # lint
|
|
203
|
+
uv run mypy # type check
|
|
204
|
+
uv run lint-imports # architectural import contracts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Documentation
|
|
208
|
+
|
|
209
|
+
Full documentation is available at the [JASIL docs site](https://jasil.endurain.com/).
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
|
|
214
|
+
|
|
215
|
+
## Contributing
|
|
216
|
+
|
|
217
|
+
Contributions welcome! See [Contributing Guidelines](CONTRIBUTING.md) for guidelines.
|
|
218
|
+
|
|
219
|
+
<div align="center">
|
|
220
|
+
<sub>Built with ❤️ from Portugal | Part of the <a href="https://github.com/endurain-project">Endurain</a> ecosystem</sub>
|
|
221
|
+
</div>
|
jasil-0.1.0/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# JASIL
|
|
2
|
+
|
|
3
|
+
[](https://github.com/endurain-project/jasil/blob/main/LICENSE.md)
|
|
4
|
+
[](https://github.com/endurain-project/jasil/releases)
|
|
5
|
+
[](https://pypi.org/project/jasil/)
|
|
6
|
+
[](https://pypi.org/project/jasil/)
|
|
7
|
+
[](https://pypi.org/project/jasil/)
|
|
8
|
+
[](https://jasil.endurain.com/)
|
|
9
|
+
[](https://github.com/endurain-project/jasil)
|
|
10
|
+
|
|
11
|
+
**J**ust **A**nother **S**ubstrate & **I**nfrastructure **L**ibrary — a
|
|
12
|
+
framework-agnostic infrastructure substrate for Python services: swappable
|
|
13
|
+
capability backends, an event pipeline, durable jobs, and observability.
|
|
14
|
+
|
|
15
|
+
> **Status:** `0.1.0` — the API is still settling. Expect breaking changes on
|
|
16
|
+
> minor versions until `1.0.0`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it gives you
|
|
21
|
+
|
|
22
|
+
| Layer | What it does |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **Providers** | Small protocols — state, storage, events, lock, clock, geocoding — that your domain code depends on instead of on Redis, S3, or Postgres. |
|
|
25
|
+
| **Backends** | A working implementation of each, selected by URI: `memory://` or `redis://`, `local://` or `s3://`, `noop://` or `postgres-advisory://`. |
|
|
26
|
+
| **Deployment profiles** | `local` runs single-process with zero extra infrastructure. `distributed` requires shared backends and refuses to guess them. |
|
|
27
|
+
| **Event pipeline** | One envelope, one publish seam, payload schema versioning that survives a rolling deploy. |
|
|
28
|
+
| **Durable jobs** | A transactional outbox relayed into leased per-subscriber jobs, with exponential backoff and a dead-letter queue. |
|
|
29
|
+
| **Observability** | An event lifecycle log and bounded retention pruning for every table it owns. |
|
|
30
|
+
|
|
31
|
+
A core install depends on **`sqlalchemy` and `pydantic` only**. Every backend
|
|
32
|
+
client lives behind an extra and is imported lazily, so a single-process
|
|
33
|
+
deployment never loads `redis`, `boto3`, or `requests`.
|
|
34
|
+
|
|
35
|
+
> **JASIL is synchronous.** Every provider, subscriber, and the durable-jobs
|
|
36
|
+
> layer is blocking Python, and the ORM integration expects a synchronous
|
|
37
|
+
> `sessionmaker` — there is no `AsyncSession` support. On an async framework,
|
|
38
|
+
> call into JASIL from a worker thread (a `def` FastAPI route or dependency does
|
|
39
|
+
> this for you). See [the docs](https://jasil.endurain.com/#one-thing-it-cannot-do-yet).
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install jasil # core: memory / local disk / in-process
|
|
45
|
+
pip install "jasil[redis,s3]" # distributed state, events and storage
|
|
46
|
+
pip install "jasil[all]" # every optional backend
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Extra | Enables |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `redis` | Redis state store and Redis-Streams event bus |
|
|
52
|
+
| `s3` | S3-compatible object storage |
|
|
53
|
+
| `postgres` | PostgreSQL advisory locks |
|
|
54
|
+
| `jobs` | Durable-job scheduling |
|
|
55
|
+
| `fastapi` | `Depends` helpers exposing the providers to routes |
|
|
56
|
+
| `geocoding` | HTTP reverse geocoding |
|
|
57
|
+
| `migrations` | Packaged Alembic revisions |
|
|
58
|
+
|
|
59
|
+
### Verifying a release
|
|
60
|
+
|
|
61
|
+
Releases are built and published by [this repository's release workflow](.github/workflows/publish-jasil.yml) through PyPI Trusted Publishing, with [PEP 740](https://peps.python.org/pep-0740/) attestations. You can confirm a downloaded artifact came from that workflow and was not substituted:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uvx pypi-attestations verify pypi \
|
|
65
|
+
--repository https://github.com/endurain-project/jasil \
|
|
66
|
+
pypi:jasil-<version>-py3-none-any.whl
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A successful run prints `OK: <filename>`. `Provenance for file ... was not found` means the artifact predates attested publishing rather than that verification failed.
|
|
70
|
+
|
|
71
|
+
Each release run also produces a CycloneDX SBOM and `SHA256SUMS`, generated from a clean install of the built wheel. These are retained as workflow artifacts on the release run rather than published to PyPI.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
JASIL never reads your environment, creates your engine, or owns your
|
|
76
|
+
declarative base — the host supplies all three.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from sqlalchemy import create_engine
|
|
80
|
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
81
|
+
|
|
82
|
+
import jasil.orm as jasil_orm
|
|
83
|
+
import jasil.settings as jasil_settings
|
|
84
|
+
from jasil.container import build_platform
|
|
85
|
+
from jasil.runtime import set_active_platform
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Base(DeclarativeBase):
|
|
89
|
+
"""Your application's declarative base."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# 1. Map JASIL's tables into your registry, before any database use.
|
|
93
|
+
jasil_orm.map_models(Base)
|
|
94
|
+
|
|
95
|
+
# 2. Hand JASIL a session factory bound to your engine.
|
|
96
|
+
engine = create_engine("postgresql+psycopg://...")
|
|
97
|
+
jasil_orm.configure_sessionmaker(sessionmaker(bind=engine))
|
|
98
|
+
|
|
99
|
+
# 3. Configure, build, publish.
|
|
100
|
+
jasil_settings.configure(jasil_settings.JasilSettings(data_dir="/srv/data"))
|
|
101
|
+
set_active_platform(build_platform())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then depend on capabilities, not on infrastructure:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from jasil.runtime import get_active_platform
|
|
108
|
+
|
|
109
|
+
platform = get_active_platform()
|
|
110
|
+
platform.state.set("session:abc", b"...", ttl_seconds=3600)
|
|
111
|
+
platform.storage.save("thumbnails", "42.webp", image_bytes)
|
|
112
|
+
|
|
113
|
+
with platform.lock.try_acquire("nightly-backfill") as acquired:
|
|
114
|
+
if acquired:
|
|
115
|
+
run_backfill()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Publishing an event goes through one seam, so switching from best-effort
|
|
119
|
+
delivery to a transactional outbox is a configuration change, not a rewrite:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from jasil.publisher import publish
|
|
123
|
+
|
|
124
|
+
publish("activity.created", {"activity_id": 42}, source="api:store_activity", db=db)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Going distributed
|
|
128
|
+
|
|
129
|
+
Change the profile and point the capability URIs at real infrastructure:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
jasil_settings.configure(
|
|
133
|
+
jasil_settings.JasilSettings(
|
|
134
|
+
profile=jasil.DeploymentProfile.DISTRIBUTED,
|
|
135
|
+
state_uri="redis://cache:6379/0",
|
|
136
|
+
events_uri="redis://cache:6379/1",
|
|
137
|
+
storage_uri="s3://my-bucket",
|
|
138
|
+
lock_uri="postgres-advisory://",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `distributed` profile **refuses to start** if a capability URI is unset. A
|
|
144
|
+
silent fallback to a process-local backend across replicas is the failure the
|
|
145
|
+
profile system exists to prevent.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv sync --all-extras --group dev
|
|
151
|
+
uv run pytest # tests + coverage gate
|
|
152
|
+
uv run ruff check . # lint
|
|
153
|
+
uv run mypy # type check
|
|
154
|
+
uv run lint-imports # architectural import contracts
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Documentation
|
|
158
|
+
|
|
159
|
+
Full documentation is available at the [JASIL docs site](https://jasil.endurain.com/).
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
|
|
164
|
+
|
|
165
|
+
## Contributing
|
|
166
|
+
|
|
167
|
+
Contributions welcome! See [Contributing Guidelines](CONTRIBUTING.md) for guidelines.
|
|
168
|
+
|
|
169
|
+
<div align="center">
|
|
170
|
+
<sub>Built with ❤️ from Portugal | Part of the <a href="https://github.com/endurain-project">Endurain</a> ecosystem</sub>
|
|
171
|
+
</div>
|
jasil-0.1.0/SECURITY.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
JASIL is at `0.x`. Security fixes are released against the latest published
|
|
6
|
+
version only; there are no maintained release branches yet. Once `1.0.0` ships,
|
|
7
|
+
this section will state a support window.
|
|
8
|
+
|
|
9
|
+
| Version | Supported |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `0.1.x` | Yes |
|
|
12
|
+
| `< 0.1` | No |
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
In scope: anything in the `jasil` package. That includes the SSRF guard on
|
|
17
|
+
outbound hosts, the path-traversal checks in the local storage backend, and any
|
|
18
|
+
way a host application's data could be exposed, corrupted, or lost through
|
|
19
|
+
JASIL's own code.
|
|
20
|
+
|
|
21
|
+
Out of scope: vulnerabilities in an application that *uses* JASIL but stem from
|
|
22
|
+
its own code or configuration — for example allow-listing an internal host via
|
|
23
|
+
`NetworkSettings.ssrf_allowed_hosts` and then dialing it. Report those to that
|
|
24
|
+
application's maintainers.
|
|
25
|
+
|
|
26
|
+
Vulnerabilities in a dependency should be reported upstream first. If JASIL's use
|
|
27
|
+
of it makes the impact materially worse, report that here too.
|
|
28
|
+
|
|
29
|
+
## Reporting a vulnerability
|
|
30
|
+
|
|
31
|
+
1. **Do not** open a public issue.
|
|
32
|
+
2. Email <joao@endurain.com> with the details.
|
|
33
|
+
3. Include:
|
|
34
|
+
- steps to reproduce;
|
|
35
|
+
- the affected version;
|
|
36
|
+
- potential impact;
|
|
37
|
+
- any suggested fix, if you have one.
|
|
38
|
+
4. You will get an acknowledgement when possible.
|
|
39
|
+
|
|
40
|
+
Please include as much detail as you can — it is what makes a report actionable
|
|
41
|
+
rather than a starting point for investigation.
|
|
42
|
+
|
|
43
|
+
## What to expect
|
|
44
|
+
|
|
45
|
+
This project is maintained by one person in their spare time, so response times
|
|
46
|
+
vary. A fix will be released as soon as it is ready, and the advisory will credit
|
|
47
|
+
you unless you would rather stay anonymous.
|
|
48
|
+
|
|
49
|
+
Thank you for helping keep this project secure.
|