Changelog¶
All notable changes to this project are documented here. ninja-devx follows semantic versioning; see the support and stability page for what that means before 1.0.
0.0.3¶
Third alpha. Adds django-filter and django-rules integrations, HTTP QUERY
operations and Django 5.2+ model field support. APIs may still change before 1.0.
Added¶
filterset_classon list endpoints: django-filterFilterSetclasses become typed, documented query parameters and run with the request (filtersextra).ninja_devx.contrib.rules.HasRule: django-rules permissions and predicates, checked on the request and on loaded objects (rulesextra).@query: HTTPQUERYoperations (a safe method with a request body), treated as reads by permissions and supported by the OpenAPI diff and the generated clients.NinjaDevXDeprecationWarning, the category future deprecations will use.- Guide for browser and mobile clients: session + CSRF for single-page apps, django-allauth headless tokens for apps, API keys for machines.
Changed¶
- Generated input schemas and
devx_scaffoldmake fields with adb_defaultoptional; creating without them (or withnullon a non-null column) lets the database fill them. - A model with a
CompositePrimaryKeyand the defaultlookup_fieldfails at startup with a hint instead of generating a lookup that cannot match. - An annotation that cannot be resolved because it names an attribute of its own class
(
def list(self, request) -> list[Out]on Python 3.14) now says so and suggests the fix. - A controller option set as a class attribute (
permissions = [...]instead ofoptions = ControllerOptions(permissions=[...])) fails at startup; it used to be ignored silently, leaving the operations unprotected. - CI's compatibility matrix resolves django-filter together with the tested Django, so the Django 4.2 rows run django-filter 25.x instead of the locked 26.x (which needs 5.2+).
0.0.2¶
Second alpha. Focuses on module boundaries, introspection, explicit query planning and a wider client generator. APIs may still change before 1.0.
Added¶
devx_inspectcommand: prints the resolved policy for a mounted controller (tenant, owner, parent, permissions, transaction, idempotency, pagination, relations and hooks) as a tree or JSON.@requires_related(...)and therelatedcontroller attribute: explicitselect_related/prefetch_relatedhints the N+1 planner applies when a schema cannot be analysed statically. System checkninja_devx.W006validates the hints.ninja_devx.cqrs:use_query(read handlers, symmetric touse_case), optionalCommand/Querymarkers,DomainEvent+EventBusdelivered after commit through aTaskQueue, andUnitOfWorkfor multi-repository transactions. No command bus or global registry.- HTTP middleware:
SecurityHeadersMiddleware(HSTS/CSP/referrer/frame options), request hardening (MaxBodySizeMiddleware,EnforceContentTypeMiddleware,JsonDepthMiddleware),ResponseCacheMiddlewarewith prefix invalidation (privatewhen varying on credentials), andPaginationHeadersMiddleware(RFC 8288Linkfor offset and cursor pagination,X-Total-Countfor counted offset pages). Rejections use the package error format. devx_openapi --against baseline.json: fail on breaking OpenAPI changes (removed operations/fields, new required input, type/enum/array item changes); additive changes are reported. With--outputthe current document is written after the comparison.- Pluggable
SearchBackendfor list search (IContainsSearch,PostgresSearch); thesearchparameter stays in the filter schema and OpenAPI. - Write-side field visibility:
WriteVisibleTo(permission)on input schemas; model controllers reject forbidden fields on create/update with 403. - Soft-delete helpers:
soft_delete_cascademarks related objects with the parent, andsoft_delete_unique(Model, "field")builds a partial unique constraint for active rows. - Abstract model bases in
ninja_devx.models:TimeStamped(created_at/updated_at),UserStamped(created_by/updated_by, filled from the request user on create, update, bulk update and import),Stamped(both) andSoftDeletable(deleted_at/deleted_by, used bySoftDeleteMixinwithout configuration). The columns areeditable=False, so generated input schemas and scaffolding leave them out. - Task queue adapters in
ninja_devx.contrib.tasks: Celery, Dramatiq, RQ, Taskiq, Temporal and FastStream behind the existingTaskQueueprotocol, plus a genericDeferredTaskQueue. ninja_devx.testing:sample/samplesbuild valid, JSON-serialisable request payloads from a schema and raiseTypeErrorfor a field type they cannot fill.devx_apikey rotateandrotate_api_keyreplace a key's secret in place; revoked keys are refused andrate_limit=Noneclears the limit.APIPlugin/install: bundle API middleware, error rules and startup work; error rules from all plugins are merged into oneErrorMap.- Client generation: OpenAPI
discriminator(tagged unions) for Python and TypeScript, multipart bodies without a required file, cookie parameters, andtext/event-streamresponses as a Python line iterator (Iterator[str]/AsyncIterator[str]). Documented non-2xx JSON responses are exposed on the parsed operation (Operation.errors). The TypeScript client rejects streaming. Clients without cookie or streaming operations regenerate unchanged. benchmarks/frameworks/: a cross-framework workload characterization over one HTTP contract (Django Ninja, ninja-devx and optional DRF/Ninja-Extra adapters); a local tool, not a CI gate.- Query tuning:
expand_ruleslets a controller filter, order and cap how many rows?expand=loads per parent (window function on Django 5+, correlated subquery on 4.2);QueryExplainMiddlewareaddsX-Query-Count/X-Query-Time/X-Query-Planheaders in development; andLimitOffsetPagination(count=...)also accepts"estimate"(PostgreSQLreltuples) and an integer threshold (X-Total-Count: N+). - Nested writes (
NestedWritesMixin,Nested) for creating/updating a parent and its child collections in one request and transaction, and change tracking (ModelController.on_change,changed_fields()) for PUT/PATCH and bulk updates. TransitionsMixin/Transitionfor API-level state transitions on model controllers (POST /{pk}/<name>,GET /{pk}/transitions) with permissions, guards, row locking and a 409invalid_transitionerror.bulk_partialonBulkCreateMixin/BulkUpdateMixin: per-item savepoints and a 207 partial-success response (results,X-Bulk-Failedheader).- A metadata endpoint (
MetaMixin), an aggregation endpoint (AggregateMixin), response versioning (VersionedResponseMixin), and OpenAPI examples generated from sample data (with_examples/openapi_examples). - Pluggable throttle storage (with a Redis-backed exact fixed window), a
RequestLogPluginfor structured per-request logs, and aSensitivefield marker applied to CRUD exports, validation-error bodies and the audit log. ninja_devx.contrib.jobs: background jobs tracked asJobrows, started withstart_job/@job, polled throughJobsController, withdevx_jobs prune/retrymaintenance (new app; add toINSTALLED_APPSand migrate).- Runtime N+1 detection (
ninja_devx.contrib.nplusone, an adapter over django-zeal) with astrict_queriestest fixture, and thedevx_doctormanagement command for configuration-risk findings beyondmanage.py check. manage.py devx_startproject: generates a runnable ninja-devx project (settings, hardened API, health check, JSON logging, DATABASE_URL-based database, Postgres compose file) with optional--appand--no-docker.
Changed¶
GrantsBackendmoved fromninja_devx.security.object_permissionstoninja_devx.contrib.grants.backends; update imports. The built-in object-permission registry resolves backends by name.- Core packages no longer import
ninja_devx.crudorninja_devx.contrib; a test enforces the boundary. - The Docker validation enforces a 90% statement/branch coverage floor
(
tools/verify_local.py --fail-under). - Documentation: new CQRS and DDD and Inspecting controllers guides, expanded typing and mounting pages, a guide link on every configuration-reference page, and a dedicated Migrations section in the navigation.
LimitOffsetPagination(count=...)widens frombooltobool | Literal["estimate"] | int; existingTrue/Falseusage is unaffected.RateThrottle(storage=...)is additive; throttles default toCacheThrottleStorageas before.
Fixed¶
- Release documentation reflects that 0.0.1 is published.
tools/messages.pyaccepts--languageso it is usable without a shipped catalog.
0.0.1¶
First alpha release candidate. The section below is the reviewed release content; publication occurs only when the matching tag passes the release workflow.
Compatibility and operational requirements¶
- Python 3.11+, Django 4.2+ and django-ninja 1.7.x (
<2); supported interpreter/framework combinations are listed in the support policy. - Core migrations are required for durable idempotency and upload lifecycle records. Contrib apps have their own migrations and optional dependencies.
- Owner/tenant/parent scope and persisted object permissions apply to built-in writes. Audit history is paginated and staff-protected by default.
- Idempotency requires independent durable claims; webhook delivery is at-least-once; upload checksum/version enforcement is configurable.
- Generated clients support JSON, simple multipart and declared text/binary responses. Cookie parameters, SSE and complex encodings fail generation explicitly.
- Internal modules were reorganized before release. See the migration notes.
Foundations¶
- Class-based controllers compiled into native
ninja.Routers:@get/@post/...with typed options, request and singleton scopes, lifecycle methods, static-first routing,mount()with versioned prefixes. - Composable permissions (
&,|,~,Also), object-level and async checks,DjangoModelPermissions,IsOwner, typed users (AuthedRequest[User],current_user). - Generic CRUD (
CRUDController[Model, Out, In]and mixins):owner_field,Instance[Model], generated filters and ordering, nestedParent, soft delete, bulk operations,Patch[In],idempotent(),atomic=True. - A small typed DI container plus dishka and svcs adapters.
- Operation hooks,
LoggingHook, OpenTelemetry. - pytest plugin with OpenAPI snapshots,
devx_scaffoldanddevx_openapi(typed TypeScript and Python clients). - mypy strict and pyright strict, with no
typing.Anyin the package.
APIs and integrations¶
Errors and layers
ErrorMap: typed exception → response rules on operations, controllers, settings (NINJA_DEVX["ERRORS"]) and APIs (install(api), using Ninja's exception handlers).- Django
ValidationError,ObjectDoesNotExistandPermissionDeniedmap to 422, 404 and 403 by default. raises=documents errors in OpenAPI and is checked against the rules.ERROR_FORMAT = "problem+json"(RFC 9457).ninja_devx.layers, HTTP-free:DomainError(NotFound,Conflict,PermissionDenied,ValidationFailed),Repository/AsyncRepositoryprotocols,ModelRepository,ModelService,dual(one method for sync and.aasync callers),RequestContext,TaskQueue(OnCommitTaskQueue,ImmediateTaskQueue,RecordingTaskQueue),after_commit,Policy/require,Selector.layers.testing:InMemoryRepository,make_context.- CRUD
service_classandselector_class, resolved from the container. use_case(decorator, Handler, command=...)operations.- Architecture recipes (HackSoft, Cosmic-lite, dishka interactors) with shared contract tests.
Dependency injection
- Operation-level injection:
Inject[T]andAnnotated[T, Resolve(fn)], validated at startup. - Container: async and async-generator factories,
aresolve,container.scope(values)for tasks and commands,close/aclose, captive dependency checks, andcheck(key, asynchronous=...). request_context(User, tenant=...)/arequest_context,aauthenticated_user,acurrent_user,arequest_user.- dishka:
DishkaResolver(container, async_container=...)for both modes,provide_controllers, startup validation.
Async and sync
mode = "sync" | "async" | "auto"(NINJA_DEVX["ASYNC_MODE"]) andasync_variantfor reusable bases. The CRUD mixins implement both modes from one class.run_sync/run_atomic: one thread hop per unit of work. Async CRUD writes take a single hop.AsyncOperationHook(around_async), blocking sync hooks (blocking = True), async lifecycle methods, async permission checks everywhere.- Hooks and DI scopes wrap whole streams; permissions are checked before the first item of async streams.
- Teaching errors:
AsyncLazyAccessError,BlockingCallWarning(WARN_BLOCKING_MS),MixedPathWarning,ASYNC_FETCH_MODE = "raise"(Django 6.1FETCH_RAISE), and the pytestAsyncDatabaseTestWarning. python -m ninja_devx.tooling.unasyncgenerates sync twins of async code (--checkfor CI).
Permissions and operations
Also(...)adds operation permissions to the controller's.IsReadOnly,as_permission(policy, User).owner_fieldfollows relations ("project__owner").Locked[Model](select_for_update).- Typed operation metadata:
meta=(...)andoperation.meta(Kind). atomic="durable"anddatabase=.ControllerPluginprotocol (on_operation,bindings,checks), via options orNINJA_DEVX["PLUGINS"].
Quality and tooling
- Django system checks (
ninja_devx.E001–E004,W003),Controller.checks(),NINJA_DEVX["CHECK_APIS"]. manage.py devx_scaffold --checkreports model/schema drift.- Query optimizer v2:
Prefetchquerysets join foreign keys inside prefetched relations;optimize_queries = "only";query_plan(). - Testing:
assert_max_hops,capture_commits/captured_commitsfixture. - Benchmarks v2: interleaved in-process overhead with thread-hop counts and an optional
budget check (
benchmarks/budget.json), plus WSGI/ASGI load tests with p50/p99 (benchmarks/load.py). - Docs: errors, layers, async and sync, system checks, recipes, support policy; every code block in the docs is compiled by the test suite.
SaaS, HTTP and schemas¶
- Multi-tenancy:
tenant_field, tenant resolvers (sync or async, settings,RequestContext,request.tenant),Parent(..., tenant_field=...),current_tenant(request). - Conditional requests:
etag = ETag()(304 on reads,If-Match→ 412/428 on writes),conditional()for any GET. CursorPaginationfollowing the request's ordering;pagination_options.- Throttles:
UserRateThrottle,AnonRateThrottle,ClientRateThrottle,ScopedRateThrottle(THROTTLE_RATES),TenantRateThrottle; thread-safe and atomic. - Role-based field visibility:
VisibleTo(...)and theFieldVisibilitymixin. - Contract tests:
ninja_contractfixture on schemathesis ([contract]extra). - Nothing hard-coded:
routes(rename, reconfigure or disable operations),lookup_param,ordering_param,SoftDelete(field, deleted=, active=, deleted_at=, deleted_by=). - Invalid lookup values reach Ninja and return JSON 422 (path converters are opt-in with
lookup_converter). - Schema generation:
devx_scaffoldwrites model constraints (max_length,min_length, patterns, ranges, decimals,help_text);devx_openapi --format pythongenerates validated pydantic models (default) or TypedDicts; drift checks flagmax_lengthmismatches; system checkW005.
Security and optional integrations¶
- Object permissions:
ObjectPermissions()on any model controller (DRFDjangoObjectPermissionssemantics: 404 withoutview, 403 without the method's permission, lists filtered in SQL), backends for the newninja_devx.contrib.grantsapp, django-guardian ([guardian]) anduser.has_perm;assign_perm,remove_perm,get_perms,get_objects_for_user,grants_for;ObjectSharingMixinendpoints;OBJECT_PERMISSION_BACKENDsetting. - Response shaping:
sparse_fields(?fields=) andExpandable()relations (?expand=), with the query planner joining only what is expanded. AutoCRUDController[Model]andAutoReadOnlyController[Model]: schemas generated with Ninja'screate_schema(schema_fields,read_only_fields,write_only_fields).LimitOffsetPagination: links that keep query parameters,count=False,max_offset, stable ordering.ExportMixin/ImportMixin: streamed CSV and JSON Lines export honoring filters and visibility; all-or-nothing imports with row-level 422 errors anddry_run.- Middleware for APIs, routers, mounts and controllers (
use_middleware,ControllerOptions(middleware=...)) on Ninja'sadd_decorator:RequestIDMiddleware,ServerTimingMiddleware,DeprecationMiddleware(RFC 9745 / RFC 8594),RateLimitHeadersMiddleware(added automatically with ninja-devx throttles). HealthController(/live,/ready) with database, cache and migration checks.ORJSONRendererandMsgspecRenderer;OpenTelemetryMetricsMiddleware.contrib.apikeys: hashed, scoped, expiring API keys (APIKeyAuth,APIKeyBearer,HasScopes,RequiresScope), a self-service controller anddevx_apikey.contrib.audit:AuditMixinrecords creates, updates (field diffs) and deletes in the same transaction, with redaction and request ids;record(),AuditLogController,AuditHistoryMixin.contrib.webhooks: transactional outbox (publish), Standard Webhooks signatures, retries with backoff, endpoint disabling,SKIP LOCKEDworkers (devx_webhooks deliver),WebhookEndpointController,verify_signaturefor receivers.contrib.uploads: presigned direct uploads (S3Signerwith[s3],FakeSigner), upload policies and ownership checks.- Webhooks hardening: SSRF protection (
URLPolicy,check_url,SafeHTTPTransportchecks the connected address, so DNS rebinding is covered too), optional encryption of signing secrets (WEBHOOK_SECRET_KEYS,[crypto],devx_webhooks generate-key/encrypt-secrets), and delivery right after commit through task queues (publish(queue=...),deliver_event,django.taskstask). - API keys: per-key
rate_limitandAPIKeyRateThrottle. sparse_fieldsoperations document<Out>Partialresponses (no required properties), so validating generated clients accept?fields=responses.- Django admin for
ObjectGrant,APIKey(revoke action),AuditEntry(read-only) and webhooks (secret shown once, rotate, retry, enable). - Translations: client-facing messages use
gettext(default_messageon domain errors,gettext_nooppermission messages), andtools/messages.pyto extract and compile catalogs without GNU gettext. English defaults ship; add catalogs for other languages. ObjectSharingMixin.validate_holder()restricts who objects may be shared with.devx_startapp: Django'sstartappwith a ninja-devx template.- "Did you mean" suggestions for misspelled fields, routes and settings.
- Project:
SECURITY.md,CONTRIBUTING.md, versioned documentation with mike, a PostgreSQL test job and SQLite/PostgreSQL load tests in CI.
Known limitations¶
- A permission denial on an async streaming operation closes the stream empty (with a logged warning) instead of returning 403: Ninja starts the response before the stream runs.
Container.instanceandContainer.overridevalidate values at registration rather than in type checkers, because mypy cannot infer the type from abstract or protocol keys.