Changelog¶
Полная история ниже (зеркало CHANGELOG.md из репозитория). Релизы с
0.1.0b1 по 0.9.0b1 были бета (0.x.0bN); 1.0.0 следует обычному semver.
Changelog¶
All notable changes to EndoCore are documented here.
[1.0.0] — 2026-07-24 — TestClient, observability, native-async PostgreSQL, and a coverage pass to 99.9%+¶
EndoCore's first stable release. The public API documented in the reference is now covered by semver going forward.
2329 tests (up from 1756), run against real PostgreSQL and Redis in CI, not
just SQLite/fakes. This pass closed the three gaps identified after the
0.9.0b1 security audit as blocking a real 1.0, then drove test coverage
across the whole framework to 99.9%+ — every uncovered line was either given a
real, behavior-asserting test or traced to something genuinely unreachable
under the current architecture (documented inline, not hand-waved). Several
real bugs surfaced purely as a side effect of that push, not from hunting for
them.
Added¶
TestClient— an in-process ASGI test client, stdlib-only (no new dependency):TestClient(app).get/post/put/patch/delete/head/options(...)for HTTP,client.websocket_connect(path)for an interactive WebSocket session, andwith TestClient(app) as client:to run the real ASGI lifespan handshake soon_startup/on_shutdownhooks execute for real. Multi-chunk streaming responses are reassembled transparently. See Testing your app.- Observability:
metrics_middleware()+MetricsExtension(Prometheus request counters/histograms,pip install endocore[metrics]) andtracing_middleware()+TracingExtension(OpenTelemetry spans correlated with the existing request-id logging,pip install endocore[otel]). Both are lazy-imported — installing neither extra costs nothing and breaks nothing. See Metrics and Tracing. - Native-async PostgreSQL (
configure(backend="postgres", ..., async_native=True)) — everya*ORM method (queries, writes, bulk operations, transactions) gets a real driver-native async body over psycopg3'sAsyncConnection, instead of the default threadpool offload. Strictly opt-in: an existing deployment's behavior never changes on upgrade, and requesting it against a backend that doesn't support it (currently SQLite) raisesConfigurationErroratconfigure()time, not on the first query. See Native async on PostgreSQL.
Fixed¶
WebSocketManager.start()race — it returned control right after spawning the Redis subscriber thread, without waiting forpsubscribe()to actually be acknowledged by the server. Abroadcast()from another worker sent immediately afterstart()(exactly what happens when several workers boot around the same time) could be silently dropped forever — Redis pub/sub never replays for a subscriber that wasn't listening yet.start()now blocks (with a timeout) until the subscription is confirmed.endo test -k <name>was broken —argparse.parse_known_args()split recognized and unrecognized tokens into two separate buckets, and reassembling them lost their relative order, so-kended up with no following value. Fixed by no longer competing with pytest's own flags for a positional argument.Manager.ain_bulk()was missing — every other asyncQuerySetmethod had aManager-level delegate (Model.objects.aget(...), etc.); this one didn't, soModel.objects.ain_bulk(...)raisedAttributeErroruntil you went through.get_queryset()explicitly.- CI never installed the
metrics/otelextras — six test files covering the middleware/extensions above were silently skipped on every CI run (could not import 'prometheus_client'/'opentelemetry'), so that code shipped with zero CI validation. Verified the fix against a real Linux container (not just reading the YAML): re-running with the extras installed turns all 75 of those tests green. WebSocketDisconnectError/TestResponseweren't exported from the top-levelendocorepackage — reachable only viaendocore.core.testclient, inconsistent withTestClientitself (and every other public class) being a top-level import. Both are now inendocore.__all__.
[0.9.0b1] — 2026-07-24 — security audit: response-splitting, cache RCE, CSWSH, ORM races, and a dev-mode-by-default footgun¶
1756 tests. This release is the result of a deliberately adversarial pass
over the whole framework — not just reading code, but reproducing each
exploit before the fix and again after, and in one case catching a regression
in the fix itself before it shipped. bandit and pip-audit now run in CI on
every push, and the request parsers (multipart, JSON, query string) were
property-fuzzed with hypothesis.
Security¶
- HTTP response splitting (CWE-113) —
ResponseandStreamingResponsenow reject (ValueError) any header name, header value, cookie component, ormedia_typecontaining a raw CR, LF, or NUL, enforced unconditionally at the point the response is built rather than left to the ASGI server to catch. - Pickle RCE in the Redis cache (CWE-502) —
RedisCache.get()calledpickle.loads()on whatever bytes Redis returned, unauthenticated; anything that could write that key got code execution on the next read. Passsecret=toCacheExtension/configure_cache("redis", ...)to HMAC-sign every value, bound to its own cache key so a signed blob can't be copied to a different key and still verify. Withoutsecret=, behavior is unchanged but now raises a warning. - Cross-site WebSocket hijacking — the handshake had no
Origincheck at all, so a page on any other site could open a websocket to your app and ride along on a cookie-based session. Same-origin is now enforced by default outsidedev=True(relaxed automatically in dev, since a local frontend on another port is a different origin); configureApplication(ws_allowed_origins=[...])explicitly for a real cross-origin frontend. create_app()defaulted todev=True— the ASGI factory thatuvicorn endocore.asgi:create_app --factory(the documented production entry point) uses defaulted dev mode on whenENDOCORE_DEVwasn't set at all, silently exposing/docs, the dev file watcher, and the relaxed websocket origin check.Applicationitself already defaulted todev=False; the factory now matches it.endo devis unaffected — it already set the env var explicitly either way.- Two ORM race conditions leaked driver exceptions under concurrency —
get_or_create()/update_or_create()andManyRelatedManager.add()(M2M) could raise an uncaughtIntegrityErrorwhen two callers raced the same not-yet-existing row/relation. Both now catch the race, verify the row/pair actually exists now, and return the existing result instead of raising — verified by reproducing the race with real concurrent threads before and after the fix. bulk_update()skipped field validation — unlikesave(),.update(), andbulk_create(), it wrote values straight to SQL with nochoices, required-ness, or custom-validator check. Arolefield restricted tochoices=[...]could be set to an arbitrary string through the bulk path.Model.__repr__printed secrets — apassword_hash-shaped column could end up readable in a strayprint(user), a traceback, or a debugger. Masked by field name, the same waySettings.__repr__already worked.- Log masking only matched exact key names —
old_password,X-Api-Key, andrefresh_tokenall bypassed masking because only a field literally namedpassword/api_key/... was ever caught. Matching is now by substring, case/separator-insensitive. - CSRF token comparison wasn't constant-time —
cookie != headeris nowhmac.compare_digest, matching every other secret comparison in the codebase. - Connection pool exhaustion hung forever —
Connection._acquire()waited on an unboundedthreading.Condition; a genuinely exhausted or stuck pool blocked every caller (and the worker thread behind it) indefinitely with no diagnostic. Addedpool_timeout(default 30s) →PoolTimeoutErrorinstead. - Background task (
Response(..., background=...)) failures vanished — an exception there is now logged (with the same request id as the original request) instead of propagating silently past an already-sent response. - Swagger UI supply-chain risk —
/docsloaded JS/CSS from an unversionedunpkg.comURL with no integrity check. Pinned to a specificswagger-ui-distversion with real, computed Subresource Integrity hashes. - Malformed request bodies crashed instead of failing clean — a non-UTF-8
multipart field, a non-Latin-1 multipart boundary, a non-UTF-8
application/x-www-form-urlencodedbody, and a pathologically deep JSON array all raised an unhandledUnicodeDecodeError/UnicodeEncodeError/RecursionErrorinstead of the framework's ownBadRequestconvention (none of these leaked info or crashed the process —logging_middleware's top-level handler already contained them — but all four now resolve to a clean 400, found by fuzzing the parsers withhypothesis). cryptographyCVE (GHSA-537c-gmf6-5ccf, vulnerable statically-linked OpenSSL) — minimum version raised from>=41to>=48.0.1.
Added¶
ip_allowlist_middleware(allowed=[...])— restrict a backend to known callers by source IP/CIDR (v4 and v6) rather than trusting a spoofable header; composes withproxy_headers_middlewarefor real client IPs behind a reverse proxy.- CI
securityjob —bandit(static analysis) andpip-audit(dependency CVEs) run on every push/PR alongside the test matrix.
Fixed¶
ManyRelatedManager.set()wasn't atomic —clear()andadd()ran as separate operations; a failure inadd()left the relation cleared instead of rolled back. Now one transaction.
[0.8.0b1] — 2026-07-23 — data migrations, distributed rate limit, WebSocket fan-out¶
1696 tests (plus PostgreSQL pool tests behind ENDOCORE_TEST_POSTGRES_DSN and a Redis-backed rate-limit/pub-sub concurrency test behind ENDOCORE_TEST_REDIS_URL — both now run for real in CI, against service containers).
Added¶
prefetch_relatedon reverse FK relations —Author.objects.prefetch_related("books")now batch-loads the reverse side in one extra query (previously only forward FK and M2M were supported; reverse access fell back to one query per instance). A bare.all()or plain iteration on the reverse manager reads the prefetched cache; any further chaining (.filter(),.exclude(), ...) re-queries, same as the forward/M2M prefetch already worked.- Data migrations —
endo makemigrations <name> --pythonwrites aforward(conn)/reverse(conn)stub numbered into the same history as schema (JSON) migrations, somigrate/rollback/showmigrationsorder and track a data transformation together with the schema change it depends on, instead of a one-off script run by hand. Runs inside its ownatomic()block; a raised exception rolls back and the migration isn't recorded as applied. Omitreverse(the generated stub raises) for a migration that can't be undone —rollbackthen fails loudly instead of doing nothing. - Distributed rate limiting —
rate_limit_middleware(..., redis_client=...)shares one counter across every worker process via Redis's atomicINCR, instead of each process enforcing its own independent in-memory limit (which under N workers silently turns a "100 req/min" limit into "100·N req/min"). Omitredis_clientand behavior is unchanged (in-memory, per-process). The Redis client's synchronous calls are offloaded viaasyncio.to_thread. - Multi-process WebSocket fan-out —
WebSocketManager(redis_client=...)publishes broadcasts to Redis (origin-tagged to avoid a worker re-delivering its own message to itself) so every worker's room delivers to its own local sockets.await manager.start()/await manager.stop()manage the background subscriber; wire them intoon_startup/on_shutdown. Withoutredis_client, both are no-ops and behavior is unchanged (single process). - CI now runs against real PostgreSQL and Redis — service containers
(
postgres:16,redis:7) with health checks, so the pool-concurrency and distributed rate-limit/pub-sub tests exercise real servers on every push instead of only when a contributor happens to run them locally.
Changed¶
endconsole script removed —endois the only entry point.endwas kept as a bash/cmd/zsh-only alias because it's a reserved word in PowerShell (begin/process/endblocks make a bareend deva parser error there); maintaining two names for one command wasn't earning its keep. Reinstall the package (pip install -e .orpip install endocore==0.8.0b1) to pick up the change; scripts/CI invokingendneed to switch toendo.
[0.7.0b2] — 2026-07-22 — README/metadata refresh¶
No code changes — PyPI has no way to update a README on an already-published release, and the README was substantially rewritten (as a proper pitch, with a Discord link) after 0.7.0b1 was uploaded. This release exists solely to carry that README to PyPI. See 0.7.0b1 just below for the actual feature set.
[0.7.0b1] — 2026-07-22 — connection pooling, aatomic(), built-in auth¶
1679 tests (plus 3 PostgreSQL pool tests behind ENDOCORE_TEST_POSTGRES_DSN).
Fixed¶
- Transaction isolation under concurrency — a transaction now holds the
connection's lock for its whole block; concurrent threads (including the
async threadpool) can no longer interleave statements inside someone else's
open
atomic(), and autocommit writes wait for the transaction to finish. Ownership is tracked with acontextvarstoken, soa*ORM calls join the transaction they were started in. - Sync handlers no longer block the event loop — plain
def handler(...)endpoints (and syncbackground=tasks) are dispatched viaasyncio.to_thread; async handlers stay on the loop. - Rate limiter memory —
rate_limit_middlewaresweeps expired client windows (at most once per window) instead of growing without bound. - Unicode case-insensitive lookups on SQLite —
iexact/icontains/... now fold non-ASCII text (Кириллица etc.): the connection registers a Unicode-awarelower()overriding SQLite's ASCII-only built-in. - ForeignKey to non-integer pks — FK assignment, row loading (
to_python), value binding (to_db) and the DDL column type now delegate to the target model's pk field, so FKs toUUIDFieldpks work end-to-end. QuerySet.in_bulktype hints no longer reference an unimported name.- FK lookups/assignment by attname —
filter(owner_id=pk)andModel(owner_id=pk)now resolve a ForeignKey by its<name>_idattname (Django-style), not just by the relation name. - SQLite results are fetched at execute time — sqlite3 cursors read
lazily and a concurrent thread's
rollback()on the shared connection reset pending statements, so a SELECT fetched after its connection went back to the pool could silently come back empty (surfaced by the shop demo's idempotency race). Results are now materialized before release.
Changed¶
/docs+/openapi.jsonare dev-only by default —Application(openapi=None)serves them only whendev=True; opt in for production withopenapi=TrueorENDOCORE_OPENAPI=1.
Added¶
aatomic()— async transaction block (async with aatomic(): ...): same semantics asatomic()(SAVEPOINT nesting included) with the lock acquisition and commit/rollback offloaded so the event loop never blocks;with atomic():on the loop thread now emits aRuntimeWarning.- Connection pooling — each alias owns a bounded pool of physical
connections (
configure(..., pool_size=N); defaults: SQLite 1, PostgreSQL 5). A transaction pins one pooled connection for its whole block, so on PostgreSQL up topool_sizetransactions run concurrently; autocommit statements borrow any free connection. PostgreSQL connections are rolled back before returning to the pool, so none sit "idle in transaction". - PostgreSQL pool race tests —
tests/orm/test_postgres_pool.py, gated byENDOCORE_TEST_POSTGRES_DSN: proves genuine transaction concurrency, no-overdraft conditional spends and UNIQUE races on a real server beforepool_size > 1is trusted in production. - Built-in auth (stdlib-only):
session_middleware(secret)— stateless cookie sessions signed with HMAC-SHA256;request.sessionis a dict, the cookie is rewritten only when modified and deleted when cleared; tampered/expired cookies degrade to an anonymous session.hash_password/verify_password/needs_rehash— scrypt (hashlib.scrypt, OWASP work factors) in a self-describing format so parameters can be raised later; constant-time verification, andverify_password(pw, None)burns a full derivation so login timing cannot enumerate which accounts exist.login(request, pk),logout(request),user_id(request)and the DI dependencyrequire_user_id(401 for anonymous) — all importable fromendocore.py.typed— the package ships inline type hints (PEP 561); type checkers no longer need stub overrides.demos/— three end-to-end example apps exercising the framework under concurrency and real payment-style requirements (not shipped in the sdist):teamboard(kanban with live WebSocket updates),booking(slot booking with a race-tested no-double-booking guarantee),shop(idempotent purchases + payment-gateway webhook, race-tested for no-overdraft spends — see its README for the PostgreSQL pool run).
[0.6.0b1] — 2026-07-03 — sixth Beta: async ORM, ws pub/sub, pydantic, migration alter/rename¶
1632 tests.
Added¶
- Async ORM — non-blocking DB access for ASGI via a threadpool offload:
aget,acreate,acount,aexists,afirst/alast,alist,aupdate/adelete,aget_or_create,abulk_create/abulk_update,aaggregate, async iteration (async for ... in qs), and instanceasave/adelete/arefresh_from_db. - WebSocket pub/sub —
WebSocketManagerwith rooms:connect/disconnect/broadcast/broadcast_json/send_to, dead-connection cleanup (single-process; pair with Redis for multi-worker). - Pydantic integration (
endocore[pydantic]) — a handler param annotated with aBaseModelis validated from the JSON body (422 with field errors on failure); its JSON schema is included in the OpenAPIrequestBody. - Migrations: column alter + rename — a changed column definition triggers a
portable table rebuild (data preserved, reversible); explicit column
rename via
endo makemigrations --rename table.old=new(RENAME COLUMN).
Notes¶
- The async ORM uses a threadpool offload over the existing sync engine — one battle-tested query path, non-blocking for both SQLite and PostgreSQL.
[0.5.0b1] — 2026-06-18 — fifth Beta: WebSockets, cache, OpenAPI, integrations, ORM/migrations maxed¶
1600 tests.
Added — framework¶
- WebSockets: file-based routing via
Socket.py(alsoWs.py); aWebSocketclass (accept,receive_text/json/bytes,send_*,iter_text/json,close); DI-aware dispatch; unknown routes rejected (4404). - Cache layer:
configure_cache/get_cachewith in-memory and Redis backends, TTL,incr, and a@cacheddecorator (sync + async). - OpenAPI 3.0:
generate_openapi, served at/openapi.jsonand Swagger UI at/docs;endo openapiwrites/prints the schema. - Service integrations (
endocore.extensions): a pluggableExtensionbase +extensions.pyloader (setup + lifespan), with shippedRedisExtension,CeleryExtension,EmailExtension(stdlib SMTP), andCacheExtension. Optional deps:endocore[redis],endocore[celery].
Added — ORM¶
- Reverse relations:
author.book_set/related_name, reverse OneToOne (author.profile). annotate()with aggregates over a field, a M2M, or a reverse FK (Author.objects.annotate(n=Count("books"))).only()/defer()(partial column fetch),bulk_update().
Added — migrations¶
- Index diffing (CREATE/DROP INDEX in migrations),
endo showmigrations,endo sqlmigrate <name>,endo migrate <target>.
[0.4.0b1] — 2026-05-30 — fourth Beta: client-usable (DI, batteries, migrations)¶
Big round toward "client usable": convenience, security, and a lot more surface. 1510 tests.
Added — framework / HTTP¶
- Dependency Injection:
Depends(...)(FastAPI-style, nested, per-request cached) + app-level providers by type or name (providers.py). Resolves string annotations viaget_type_hints. - Unified config: typed
Settings(env-backed, secret-masked repr),env(),load_dotenv(). - HTTP exception classes:
NotFound,Unauthorized,Forbidden(=PermissionDenied),BadRequest,Conflict,UnprocessableEntity,TooManyRequests,PayloadTooLarge,MethodNotAllowed— rendered centrally. - Request:
QueryParams(.get/.getlist),cookies,form()+files()(urlencoded + multipart parser),get_signed_cookie, body-size limit. - Response:
set_cookie/delete_cookie/set_signed_cookie,redirect,no_content, background tasks; signed cookies (Signer, HMAC-SHA256). - Lifecycle:
on_startup/on_shutdownhooks (hooks.py); background tasks. - Logging: framework
X-Request-ID, colored dev logs. - Middleware bundle:
cors,security_headers,gzip,proxy_headers(trusted proxy),rate_limit,timeout,csrf(signed double-submit).
Added — ORM¶
- ManyToManyField (auto through table,
add/remove/set/clear/all/count), OneToOneField, prefetch_related (batch, no N+1), abstract models with field inheritance,Meta.ordering/unique_together/indexes. - Migrations with rollback:
endo makemigrations/migrate/rollback(forward+reverse SQL, applied-state table). refresh_from_db,save(update_fields=...),none(),in_bulk(),__contains__, richer__repr__,on_delete(CASCADE/SET NULL/RESTRICT/ PROTECT), transaction savepoints for nestedatomic().
Added — CLI¶
endo new,endo routes,endo check,endo doctor,endo makemigrations/endo migrate/endo rollback.
Fixed¶
__eq__for unsaved instances (identity),bulk_createnow backfills pks on SQLite,values_list()returns tuples (was dicts).- SQLite
LIKEmade case-sensitive (PRAGMA case_sensitive_like) socontains/icontainsbehave identically to PostgreSQL. - Column DDL emits
DEFAULTfor constant defaults (soADD COLUMN NOT NULLworks in migrations); SQLitecheck_same_thread=False+ a connection lock.
[0.3.0b1] — 2026-05-02 — third Beta: ORM completeness, encrypted files, deferrals¶
Added — ORM¶
- Many more field types:
SmallIntegerField,PositiveSmallIntegerField,PositiveIntegerField,PositiveBigIntegerField,BigAutoField,SlugField,EmailField,URLField,GenericIPAddressField,UUIDField,JSONField(JSONB on Postgres),BinaryField,TimeField,DurationField. - Validators run on the write path (
save/create/update):choices, length, positivity, email/URL/slug/IP formats;full_clean(). - Encrypted
FileField— files stored in any folder, encrypted at rest with AES-256-GCM. The DB keeps only an opaque key; if the storage leaks the files are unrecoverable without the separate key, and tampering is detected.configure_storage(root=..., key=...),generate_key(). - Relational queries: cross-table lookups (
filter(city__country__name=...)) and ordering across relations via LEFT JOINs, plusselect_related(...). - Expressions:
F(update(views=F('views') + 1)) and aggregatesaggregate(Count/Sum/Avg/Min/Max). - QuerySet:
distinct(),earliest()/latest(),get_or_create(),update_or_create(), single-statementbulk_create(),db_index+ index DDL.
Added — framework (first-Beta deferrals)¶
- Default-version alias (opt-in):
Application(default_version="latest")/endo dev --default-version latestresolves a version-less path to the newest version and logs which one served it (strict 404 stays the default). - Request streaming (
Request.stream()) andStreamingResponse. - In-process dev watcher (
watchfiles) rebuilds the route tree on change without a process restart (Application.reload()). endo testnow passes pytest flags directly (endo test -q -k name).
Changed¶
- Optional extras:
endocore[files](cryptography),endocore[watch]. - Version -> 0.3.0b1. 102 tests total.
[0.2.0b1] — 2026-04-17 — second Beta: the ORM¶
A small, secure, Django-flavoured ORM for SQLite and PostgreSQL.
Added¶
- Models — declarative
Modelclasses with a metaclass and_meta; fields:AutoField,IntegerField,BigIntegerField,CharField,TextField,BooleanField,FloatField,DecimalField,DateTimeField(auto_now),DateField,ForeignKey(lazy related-object load). - QuerySet — lazy & chainable:
filter/exclude/get/all/order_by/values/ values_list/count/exists/first/last/create/update/delete/bulk_create, slicing forLIMIT/OFFSET, and Q objects (&,|,~). - Lookups —
exact iexact contains icontains startswith istartswith endswith iendswith gt gte lt lte in isnull range. - Backends —
sqlite(stdlib) andpostgres(psycopg), sharing one security-critical base; correct per-dialect placeholders, quoting, autoincrement andRETURNING/lastrowid. - Connections & transactions —
configure()/connect(), lazy open,with atomic():blocks; credentials never logged. - Schema —
create_table/create_all/drop_tableDDL from models. - Security (the focus) — values are always driver-bound (never formatted into
SQL); identifiers validated (
^[A-Za-z_]\w*$) and quoted; lookups are a strict whitelist; LIKE wildcards in user input escaped withESCAPE;LIMIT/OFFSETcoerced to ints. 34 ORM tests including explicit injection tests. - Example —
Postmodel + ORM-backedGET/POST /v1/postendpoints.
Changed¶
endocore[postgres]optional extra for the psycopg driver.
[0.1.0b1] — 2026-04-08 — first Beta¶
First usable Beta. The framework boots a real app, serves it over uvicorn, and the CLI scaffolds and versions the tree.
Added¶
- File-based routing — folder = URL segment, file = HTTP method,
[id]= dynamic segment. Single tree-walk (Api/viarglob) builds a cached route trie at boot. - Versioning — first path segment
^v\d+$;v1/v2coexist. A request without a version prefix is a 404 (explicit over implicit). - Own
Request/Responseover the raw ASGI scope (no Starlette). - Middleware chain (onion /
call_next). Built-in logging middleware is always outermost; user middleware is auto-loaded fromMiddleware/__init__.py(an orderedmiddlewareslist). HTTPError— handlers canraise HTTPError(status, detail)to short-circuit with a status code.- Logging — stdlib wrapper; every request logged with timing and a
masked payload (
password,token,authorization, … →***). - Boot resilience — one broken handler file is collected into a boot-error summary, not fatal.
- CLI
endo—create,dev(uvicorn + reload),version create/list,test.version createcopies endpoints + local services (never globalServices/) and rewritesApi.vSRC→Api.vDESTimports so a new version uses its own local services (real isolation). - Framework test suite (
tests/, 29 tests) and a runnableexample/app.
Known limitations¶
endo testneeds--before pytest flags:endo test -- -q -k name.- No default-to-latest version alias, no request-body streaming, no
pydanticvalidation, no customwatchfileswatcher (uses uvicorn reload). These are intentionally deferred.