cello-framework 1.2.0__tar.gz → 1.2.2__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.
- {cello_framework-1.2.0 → cello_framework-1.2.2}/.github/workflows/publish.yml +1 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/CLAUDE.md +4 -2
- {cello_framework-1.2.0 → cello_framework-1.2.2}/Cargo.lock +1 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/Cargo.toml +1 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/PKG-INFO +1 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/overview.md +2 -2
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/guides/error-handling.md +16 -33
- cello_framework-1.2.2/docs/releases/v1.2.1.md +167 -0
- cello_framework-1.2.2/docs/releases/v1.2.2.md +131 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/mkdocs.yml +2 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/pyproject.toml +1 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/__init__.py +6 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/guards.py +12 -6
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/lib.rs +48 -50
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/auth.rs +4 -4
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/csrf.rs +4 -3
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/guards.rs +2 -2
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/mod.rs +13 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/rate_limit.rs +5 -5
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/session.rs +3 -3
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/minijinja_engine.rs +5 -5
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/test_cello.py +3 -3
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/test_minijinja.py +1 -1
- {cello_framework-1.2.0 → cello_framework-1.2.2}/.github/workflows/ci.yml +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/.github/workflows/docs.yml +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/.gitignore +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/CONTRIBUTING.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/ENTERPRISE_ROADMAP.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/LICENSE +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/PUBLISHING.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/README.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/README.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/benchmark.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/README.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/apps/__init__.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/apps/blacksheep_app.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/apps/cello_app.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/apps/fastapi_app.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/apps/robyn_app.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/requirements.txt +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/compare/run_benchmarks.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/benchmarks/quick_bench.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/cello.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/community/code-of-conduct.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/community/contributing.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/community/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/community/support.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/deployment/docker.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/deployment/kubernetes.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/deployment/service-mesh.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/integration/database.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/integration/graphql.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/integration/grpc.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/integration/message-queues.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/observability/health-checks.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/observability/metrics.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/observability/opentelemetry.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/observability/tracing.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/enterprise/roadmap.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/background-tasks.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/file-storage.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/fullstack.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/graphql.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/microservices.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/realtime-dashboard.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/advanced/redis-caching.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/database.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/forms.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/hello-world.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/jwt-auth.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/query-params.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/basic/rest-api.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/api-gateway.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/event-sourcing.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/health-checks.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/multi-tenant.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/oauth2.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/enterprise/rate-limiting.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/adaptive-rate-limiting.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/advanced-middleware.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/advanced-patterns.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/all-features.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/api-protocols.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/async-handlers.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/auto-openapi.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/blueprints-advanced.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/circuit-breaker.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/cluster-demo.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/complete-showcase.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/comprehensive-demo.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/database-demo.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/dto-validation.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/enterprise-config.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/guards.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/hello-world.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/lifecycle-hooks.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/middleware-demo.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-advanced.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-basic.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-blog.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-emails.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-forms.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/minijinja-macros.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/security.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/simple-api.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/smart-caching.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/examples/real/streaming-sse.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/background-tasks.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/dependency-injection.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/dto-validation.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/file-uploads.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/static-files.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/advanced/templates.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/core/async.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/core/blueprints.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/core/requests.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/core/responses.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/core/routing.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/caching.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/circuit-breaker.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/compression.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/cors.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/logging.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/overview.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/middleware/rate-limiting.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/minijinja-templates.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/realtime/sse.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/realtime/websocket.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/authentication.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/csrf.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/guards.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/headers.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/jwt.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/security/sessions.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/features/templates.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/configuration.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/first-app.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/installation.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/project-structure.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/getting-started/quickstart.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/includes/abbreviations.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/javascripts/extra.js +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/guides/best-practices.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/guides/deployment.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/guides/performance.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/guides/testing.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/patterns/cqrs.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/patterns/event-driven.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/patterns/repository.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/patterns/service-layer.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/tutorials/auth-system.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/tutorials/chat-app.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/tutorials/microservices.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/learn/tutorials/rest-api.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/logo-full.png +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/logo-icon.svg +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/logo.jpg +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/logo.svg +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/overrides/main.html +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/app.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/blueprint.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/context.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/guards.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/middleware.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/request.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/api/response.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/cli.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/config/middleware.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/config/security.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/config/server.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/errors.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/reference/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/changelog.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/index.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/migration.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.10.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.3.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.4.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.5.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.6.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.7.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.8.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v0.9.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v1.0.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v1.0.1.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v1.1.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/releases/v1.2.0.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/requirements.txt +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/stylesheets/extra.css +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/docs/tags.md +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/adaptive_rate_limit.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/advanced.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/advanced_middleware.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/advanced_patterns_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/all_features_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/api_protocols_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/async_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/auto_openapi_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/circuit_breaker.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/cluster_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/complete_showcase.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/comprehensive_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/database_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/dto_validation.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/enterprise.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/guards.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/hello.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/lifecycle_hooks.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/middleware_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_advanced.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_basic.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_blog.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_emails.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_forms.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/minijinja_macros.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/security.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/simple_api.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/smart_caching.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/examples/streaming_demo.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/cqrs.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/database.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/eventsourcing.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/graphql.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/grpc.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/messaging.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/saga.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/python/cello/validation.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/arena.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/background.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/blueprint.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/context.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/dependency.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/dto.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/error.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/handler.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/http_client.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/json.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/lifecycle.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/body_limit.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/cache.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/circuit_breaker.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/cors.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/cqrs.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/database.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/etag.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/eventsourcing.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/exception_handler.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/graphql.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/grpc.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/health.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/messaging.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/prometheus.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/redis.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/request_id.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/saga.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/security.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/static_files.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/middleware/telemetry.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/multipart.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/openapi.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/request/mod.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/request/multipart_streaming.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/request/parsing.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/response/mod.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/response/streaming.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/response/xml.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/router.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/routing/constraints.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/routing/mod.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/server/cluster.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/server/mod.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/server/protocols.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/sse.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/template.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/timeout.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/src/websocket.rs +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_adaptive.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_caching.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_circuit_breaker.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_dto.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_guards_impl.py +0 -0
- {cello_framework-1.2.0 → cello_framework-1.2.2}/tests/verify_lifecycle.py +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Cello** is an ultra-fast, Rust-powered Python async web framework designed to achieve C-level performance on the hot path while maintaining Python's developer experience. It's the successor to frameworks like FastAPI, Robyn, and Litestar, combining their best features with pure Rust implementation for maximum performance.
|
|
6
6
|
|
|
7
|
-
**Version:** 1.
|
|
7
|
+
**Version:** 1.2.2
|
|
8
8
|
**License:** MIT
|
|
9
9
|
**Python Requirement:** 3.12+
|
|
10
10
|
**Author:** Jagadeesh Katla
|
|
@@ -336,7 +336,7 @@ from cello import App, ProblemDetails
|
|
|
336
336
|
@app.exception_handler(ValueError)
|
|
337
337
|
def handle_value_error(request, exc):
|
|
338
338
|
return ProblemDetails(
|
|
339
|
-
|
|
339
|
+
type_uri="/errors/validation",
|
|
340
340
|
title="Validation Error",
|
|
341
341
|
status=400,
|
|
342
342
|
detail=str(exc),
|
|
@@ -346,6 +346,8 @@ def handle_value_error(request, exc):
|
|
|
346
346
|
|
|
347
347
|
## Version History
|
|
348
348
|
|
|
349
|
+
- **v1.2.2**: Security & bug fixes — CSRF `HttpOnly` on double-submit cookie (critical, broke all AJAX CSRF); all middleware `skip_path` prefix bypass fixed via `path_matches_skip()` helper; `FixedWindowStore` window_start never updated after reset; unused `mut` cleaned in minijinja tests
|
|
350
|
+
- **v1.2.1**: Bug fixes — server port never bound (`pyo3_asyncio` replaced with native `py.allow_threads` + `tokio::block_on`); `ProblemDetails` was missing from Python module export; `And`/`Or` guards now accept both `*args` and list styles; CSRF `HttpOnly` removed from double-submit cookie (JS must read it); `FixedWindowStore` window_start never updated after reset; all middleware `skip_path` used raw `starts_with` allowing prefix bypass; doc corrections (`type_uri` not `type_url`)
|
|
349
351
|
- **v1.2.0**: Bug fixes (shutdown coroutine never awaited, KeyboardInterrupt in shutdown handler, `request.redis` AttributeError); Redis Lua scripting (`eval`, `evalsha`, `script_load`); Rust-native `AsyncClient` backed by `reqwest + Tokio` — GIL never held during HTTP I/O, HTTP/2, gzip, rustls
|
|
350
352
|
- **v1.1.0**: MiniJinja Jinja2-compatible template engine (`MiniJinjaEngine`, `App.enable_templates()`, `App.render()`, `App.render_string()`); minijinja 2 Rust crate; HTML auto-escaping; globals; 47 new tests; 6 examples
|
|
351
353
|
- **v1.0.1**: Cross-platform fixes (Windows multi-worker, signal handling, UNC paths; ARM JSON fallback; Linux-only CPU affinity), async compatibility fixes (handler validation, guards, cache decorator, blueprints), guards and database exports in `__all__`
|
|
@@ -270,8 +270,8 @@ admin_only = RoleGuard(["admin"])
|
|
|
270
270
|
can_edit = PermissionGuard(["posts:edit"])
|
|
271
271
|
|
|
272
272
|
# Composable guards
|
|
273
|
-
admin_or_editor = Or(RoleGuard(["admin"]), RoleGuard(["editor"]))
|
|
274
|
-
admin_with_write = And(RoleGuard(["admin"]), PermissionGuard(["write"]))
|
|
273
|
+
admin_or_editor = Or([RoleGuard(["admin"]), RoleGuard(["editor"])])
|
|
274
|
+
admin_with_write = And([RoleGuard(["admin"]), PermissionGuard(["write"])])
|
|
275
275
|
|
|
276
276
|
@app.get("/admin", guards=[admin_only])
|
|
277
277
|
def admin_panel(request):
|
|
@@ -116,49 +116,32 @@ def handle_unexpected(request, exc):
|
|
|
116
116
|
### Using ProblemDetails in Cello
|
|
117
117
|
|
|
118
118
|
```python
|
|
119
|
-
from cello import App,
|
|
119
|
+
from cello import App, ProblemDetails
|
|
120
120
|
|
|
121
121
|
app = App()
|
|
122
122
|
|
|
123
|
-
def problem(type_url: str, title: str, status: int,
|
|
124
|
-
detail: str = None, instance: str = None, **extra) -> Response:
|
|
125
|
-
"""Helper to build an RFC 7807 response."""
|
|
126
|
-
body = {"type": type_url, "title": title, "status": status}
|
|
127
|
-
if detail:
|
|
128
|
-
body["detail"] = detail
|
|
129
|
-
if instance:
|
|
130
|
-
body["instance"] = instance
|
|
131
|
-
body.update(extra)
|
|
132
|
-
resp = Response.json(body, status=status)
|
|
133
|
-
resp.set_header("Content-Type", "application/problem+json")
|
|
134
|
-
return resp
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Use the helper in handlers and exception handlers:
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
123
|
@app.get("/users/{id}")
|
|
141
124
|
def get_user(request):
|
|
142
125
|
user = find_user(request.params["id"])
|
|
143
126
|
if not user:
|
|
144
|
-
return
|
|
145
|
-
|
|
127
|
+
return ProblemDetails(
|
|
128
|
+
type_uri="/errors/not-found",
|
|
146
129
|
title="User Not Found",
|
|
147
130
|
status=404,
|
|
148
131
|
detail=f"No user with ID {request.params['id']}",
|
|
149
132
|
instance=request.path,
|
|
150
|
-
)
|
|
133
|
+
).to_response()
|
|
151
134
|
return user
|
|
152
135
|
|
|
153
136
|
@app.exception_handler(ValueError)
|
|
154
137
|
def handle_validation(request, exc):
|
|
155
|
-
return
|
|
156
|
-
|
|
138
|
+
return ProblemDetails(
|
|
139
|
+
type_uri="/errors/validation",
|
|
157
140
|
title="Validation Error",
|
|
158
141
|
status=400,
|
|
159
142
|
detail=str(exc),
|
|
160
143
|
instance=request.path,
|
|
161
|
-
)
|
|
144
|
+
).to_response()
|
|
162
145
|
```
|
|
163
146
|
|
|
164
147
|
---
|
|
@@ -213,22 +196,22 @@ class ConflictError(Exception):
|
|
|
213
196
|
|
|
214
197
|
@app.exception_handler(NotFoundError)
|
|
215
198
|
def handle_not_found(request, exc):
|
|
216
|
-
return
|
|
217
|
-
|
|
199
|
+
return ProblemDetails(
|
|
200
|
+
type_uri="/errors/not-found",
|
|
218
201
|
title=f"{exc.resource} Not Found",
|
|
219
202
|
status=404,
|
|
220
203
|
detail=str(exc),
|
|
221
204
|
instance=request.path,
|
|
222
|
-
)
|
|
205
|
+
).to_response()
|
|
223
206
|
|
|
224
207
|
@app.exception_handler(ConflictError)
|
|
225
208
|
def handle_conflict(request, exc):
|
|
226
|
-
return
|
|
227
|
-
|
|
209
|
+
return ProblemDetails(
|
|
210
|
+
type_uri="/errors/conflict",
|
|
228
211
|
title="Conflict",
|
|
229
212
|
status=409,
|
|
230
213
|
detail=str(exc),
|
|
231
|
-
)
|
|
214
|
+
).to_response()
|
|
232
215
|
```
|
|
233
216
|
|
|
234
217
|
Then raise them in handlers:
|
|
@@ -253,12 +236,12 @@ from cello.guards import GuardError
|
|
|
253
236
|
|
|
254
237
|
@app.exception_handler(GuardError)
|
|
255
238
|
def handle_guard_error(request, exc):
|
|
256
|
-
return
|
|
257
|
-
|
|
239
|
+
return ProblemDetails(
|
|
240
|
+
type_uri="/errors/access-denied",
|
|
258
241
|
title="Access Denied",
|
|
259
242
|
status=exc.status_code,
|
|
260
243
|
detail=exc.message,
|
|
261
|
-
)
|
|
244
|
+
).to_response()
|
|
262
245
|
```
|
|
263
246
|
|
|
264
247
|
---
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: v1.2.1 Release Notes
|
|
3
|
+
description: Cello Framework v1.2.1 — Critical Bug Fixes
|
|
4
|
+
tags:
|
|
5
|
+
- v1.2.1
|
|
6
|
+
- Release Notes
|
|
7
|
+
- Bug Fixes
|
|
8
|
+
- Changelog
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Cello v1.2.1 — Critical Bug Fixes
|
|
12
|
+
|
|
13
|
+
**Release Date:** June 14, 2026
|
|
14
|
+
**License:** MIT
|
|
15
|
+
**Python:** 3.12+
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Cello v1.2.1 is a patch release fixing critical bugs discovered after v1.2.0: the HTTP server
|
|
22
|
+
never bound its port, `ProblemDetails` was inaccessible from Python, and `And`/`Or` guards
|
|
23
|
+
rejected the `And(g1, g2)` call style. All changes are fully backwards-compatible.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Bug Fixes
|
|
28
|
+
|
|
29
|
+
### Server Port Never Bound
|
|
30
|
+
|
|
31
|
+
**Symptom:** `app.run()` started without error but the server never listened on any port —
|
|
32
|
+
`ss -tlnp` showed nothing, all connections were refused.
|
|
33
|
+
|
|
34
|
+
**Root cause:** `pyo3_asyncio::tokio::run` drives Tokio socket I/O through Python's asyncio
|
|
35
|
+
selector loop. In Python 3.12+ with pyo3 0.20, the two event loops do not integrate correctly:
|
|
36
|
+
the asyncio loop never ticks the Tokio I/O reactor, so `TcpListener::accept()` never resolves
|
|
37
|
+
and the port never appears in the OS socket table.
|
|
38
|
+
|
|
39
|
+
**Fix:** Replaced `pyo3_asyncio::tokio::run(py, ...)` with `py.allow_threads(|| runtime.block_on(...))`.
|
|
40
|
+
The GIL is released for the duration of the server run; Tokio drives all I/O natively. Python
|
|
41
|
+
handlers re-acquire the GIL individually via `Python::with_gil` when invoked.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Before (broken — port never bound)
|
|
45
|
+
app.run(port=8000) # silently hangs
|
|
46
|
+
|
|
47
|
+
# After (fixed)
|
|
48
|
+
app.run(port=8000) # binds and serves immediately
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `ProblemDetails` Not Importable
|
|
54
|
+
|
|
55
|
+
**Symptom:** `from cello import ProblemDetails` raised `ImportError`.
|
|
56
|
+
|
|
57
|
+
**Root cause:** `ProblemDetails` had `#[pyclass]` in `src/error.rs` but was never registered
|
|
58
|
+
with `m.add_class::<error::ProblemDetails>()` in the PyO3 module, and was missing from
|
|
59
|
+
`python/cello/__init__.py`.
|
|
60
|
+
|
|
61
|
+
**Fix:** Added to module registration and Python exports.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Now works
|
|
65
|
+
from cello import ProblemDetails
|
|
66
|
+
|
|
67
|
+
@app.exception_handler(ValueError)
|
|
68
|
+
def handle_error(request, exc):
|
|
69
|
+
return ProblemDetails(
|
|
70
|
+
type_uri="/errors/validation",
|
|
71
|
+
title="Validation Error",
|
|
72
|
+
status=400,
|
|
73
|
+
detail=str(exc),
|
|
74
|
+
instance=request.path,
|
|
75
|
+
).to_response()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
!!! note "Field name is `type_uri`, not `type_url`"
|
|
79
|
+
The constructor parameter follows the RFC 7807 naming convention.
|
|
80
|
+
Use `type_uri=`, not `type_url=`.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `And`/`Or` Guards Rejected `*args` Call Style
|
|
85
|
+
|
|
86
|
+
**Symptom:** `And(guard1, guard2)` raised `TypeError: And.__init__() takes 2 positional arguments but 3 were given`.
|
|
87
|
+
|
|
88
|
+
**Root cause:** `And` and `Or` only accepted a single list argument.
|
|
89
|
+
|
|
90
|
+
**Fix:** Both now accept either a list or `*args`.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Both styles now work
|
|
94
|
+
admin_and_write = And([RoleGuard(["admin"]), PermissionGuard(["write"])])
|
|
95
|
+
admin_and_write = And(RoleGuard(["admin"]), PermissionGuard(["write"]))
|
|
96
|
+
|
|
97
|
+
admin_or_editor = Or([RoleGuard(["admin"]), RoleGuard(["editor"])])
|
|
98
|
+
admin_or_editor = Or(RoleGuard(["admin"]), RoleGuard(["editor"]))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### CSRF Cookie Readable by JavaScript (Security)
|
|
104
|
+
|
|
105
|
+
**Symptom:** AJAX/SPA applications using CSRF protection failed all POST/PUT/DELETE requests with 403.
|
|
106
|
+
|
|
107
|
+
**Root cause:** The CSRF double-submit cookie was set with the `HttpOnly` flag. This prevents
|
|
108
|
+
JavaScript from reading the cookie value, making it impossible for the frontend to copy it into
|
|
109
|
+
the `X-CSRF-Token` header — which is the entire point of the double-submit pattern.
|
|
110
|
+
|
|
111
|
+
**Fix:** Removed `HttpOnly` from the CSRF cookie. The CSRF cookie is intentionally readable by
|
|
112
|
+
JavaScript; the session cookie (where sensitive data lives) remains `HttpOnly`.
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
// Now works — JS can read the CSRF cookie and send it back
|
|
116
|
+
const csrf = document.cookie.match(/_csrf=([^;]+)/)?.[1];
|
|
117
|
+
fetch("/api/data", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "X-CSRF-Token": csrf },
|
|
120
|
+
body: JSON.stringify(data),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### Rate Limiter Fixed-Window Counter Reset (Bug)
|
|
127
|
+
|
|
128
|
+
**Symptom:** After the first rate-limit window expired, the fixed-window counter reset to zero
|
|
129
|
+
on every single subsequent request, making rate limiting completely ineffective.
|
|
130
|
+
|
|
131
|
+
**Root cause:** When the time window rolled over, `entry.count` was reset to 0 but
|
|
132
|
+
`entry.window_start` was never updated to the new window. Every request in the new window
|
|
133
|
+
saw `window_start != current_window` and reset the counter again.
|
|
134
|
+
|
|
135
|
+
**Fix:** Update `entry.window_start = current_window` alongside the count reset.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Auth Skip-Path Prefix Bypass (Security)
|
|
140
|
+
|
|
141
|
+
**Symptom:** Calling `skip_path("/health")` to exclude the health endpoint from authentication
|
|
142
|
+
also excluded `/healthz`, `/healthy`, and any other path starting with `/health`.
|
|
143
|
+
|
|
144
|
+
**Root cause:** Path matching used `request.path.starts_with(skip_path)` without checking that
|
|
145
|
+
the following character is `/` or end-of-string.
|
|
146
|
+
|
|
147
|
+
**Fix:** Path is now only skipped when it equals the pattern exactly or starts with
|
|
148
|
+
`{pattern}/`. Affects `JwtAuth`, `BasicAuth`, `ApiKeyAuth`, `RateLimitMiddleware`,
|
|
149
|
+
`SessionMiddleware`, `CsrfMiddleware`, and `GuardsMiddleware`.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Before: /healthz would also be skipped (security bypass)
|
|
153
|
+
jwt.skip_path("/health")
|
|
154
|
+
|
|
155
|
+
# After: only /health and /health/* are skipped
|
|
156
|
+
jwt.skip_path("/health")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Upgrade
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install --upgrade cello-framework==1.2.1
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
No API changes — drop-in replacement for v1.2.0.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: v1.2.2 Release Notes
|
|
3
|
+
description: Cello Framework v1.2.2 — Security & Bug Fixes
|
|
4
|
+
tags:
|
|
5
|
+
- v1.2.2
|
|
6
|
+
- Release Notes
|
|
7
|
+
- Security
|
|
8
|
+
- Bug Fixes
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Cello v1.2.2 — Security & Bug Fixes
|
|
12
|
+
|
|
13
|
+
**Release Date:** June 14, 2026
|
|
14
|
+
**License:** MIT
|
|
15
|
+
**Python:** 3.12+
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Cello v1.2.2 is a patch release that fixes one critical security bug, one security
|
|
22
|
+
vulnerability, and two correctness bugs discovered during a thorough code review of v1.2.1.
|
|
23
|
+
All changes are fully backwards-compatible.
|
|
24
|
+
|
|
25
|
+
**Upgrade immediately** if you use CSRF protection or authentication middleware skip-paths.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Security Fixes
|
|
30
|
+
|
|
31
|
+
### CSRF Double-Submit Cookie Was `HttpOnly` (Critical)
|
|
32
|
+
|
|
33
|
+
**Symptom:** AJAX applications and SPAs using `CsrfMiddleware` received 403 on every
|
|
34
|
+
POST/PUT/DELETE request, even when the CSRF token was correctly set.
|
|
35
|
+
|
|
36
|
+
**Root cause:** The CSRF double-submit cookie was set with `HttpOnly`, which prevents
|
|
37
|
+
JavaScript from reading cookie values. The double-submit pattern *requires* JavaScript
|
|
38
|
+
to read the cookie and copy its value into the `X-CSRF-Token` request header. With
|
|
39
|
+
`HttpOnly` set, this was impossible — every state-changing request failed CSRF validation.
|
|
40
|
+
|
|
41
|
+
**Fix:** Removed `HttpOnly` from the CSRF token cookie. The CSRF cookie is intentionally
|
|
42
|
+
readable by JavaScript (that is the design). Your session cookie, which holds sensitive
|
|
43
|
+
data, remains `HttpOnly`.
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
// Now works — JavaScript can read the CSRF cookie
|
|
47
|
+
const csrf = document.cookie.match(/_csrf=([^;]+)/)?.[1];
|
|
48
|
+
|
|
49
|
+
fetch("/api/submit", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "X-CSRF-Token": csrf, "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ name: "Alice" }),
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Auth Middleware `skip_path` Allowed Prefix Bypass (Security)
|
|
59
|
+
|
|
60
|
+
**Symptom:** Calling `jwt.skip_path("/health")` to exclude the health-check endpoint
|
|
61
|
+
from authentication also excluded `/healthz`, `/healthy`, `/health-admin`, and any
|
|
62
|
+
other path whose string representation started with `/health`.
|
|
63
|
+
|
|
64
|
+
**Root cause:** All middleware used `request.path.starts_with(skip_path)` with no
|
|
65
|
+
boundary check. An attacker aware of a skipped path could craft a URL that bypasses
|
|
66
|
+
authentication.
|
|
67
|
+
|
|
68
|
+
**Affected middleware:** `JwtAuth`, `BasicAuth`, `ApiKeyAuth`, `RateLimitMiddleware`,
|
|
69
|
+
`SessionMiddleware`, `CsrfMiddleware`, `GuardsMiddleware`.
|
|
70
|
+
|
|
71
|
+
**Fix:** Paths are now only skipped when:
|
|
72
|
+
|
|
73
|
+
- The path **exactly equals** the pattern, or
|
|
74
|
+
- The path **starts with** `{pattern}/` (sub-path match).
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Before: /healthz was also skipped — security bypass
|
|
78
|
+
jwt_auth.skip_path("/health")
|
|
79
|
+
|
|
80
|
+
# After: only /health and /health/* are skipped
|
|
81
|
+
jwt_auth.skip_path("/health")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Bug Fixes
|
|
87
|
+
|
|
88
|
+
### Rate Limiter Fixed-Window Counter Broke After First Window
|
|
89
|
+
|
|
90
|
+
**Symptom:** After the first time-window expired, the `FixedWindowStore` rate limiter
|
|
91
|
+
stopped enforcing limits entirely — every request was allowed regardless of how many
|
|
92
|
+
had been made.
|
|
93
|
+
|
|
94
|
+
**Root cause:** When the window rolled over, the per-client entry's `count` was reset
|
|
95
|
+
to 0 but `window_start` was never updated to the new window ID. Every subsequent
|
|
96
|
+
request also saw `window_start != current_window` and reset the counter to 0 again,
|
|
97
|
+
making the limiter permanently ineffective after the first window.
|
|
98
|
+
|
|
99
|
+
**Fix:** `window_start` is now updated to `current_window` alongside the count reset.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### Unused `mut` Warnings in Template Engine Tests
|
|
104
|
+
|
|
105
|
+
Five test functions in `minijinja_engine.rs` declared `let mut env = Environment::new()`
|
|
106
|
+
where `mut` was not needed (the variable was never mutated after construction). These
|
|
107
|
+
were cleaned up to eliminate Clippy warnings.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Files Changed
|
|
112
|
+
|
|
113
|
+
| File | Change |
|
|
114
|
+
|------|--------|
|
|
115
|
+
| `src/middleware/csrf.rs` | Remove `HttpOnly` from CSRF cookie |
|
|
116
|
+
| `src/middleware/mod.rs` | Add `path_matches_skip()` shared helper |
|
|
117
|
+
| `src/middleware/auth.rs` | Use `path_matches_skip()` in JWT/Basic/ApiKey |
|
|
118
|
+
| `src/middleware/session.rs` | Use `path_matches_skip()` |
|
|
119
|
+
| `src/middleware/rate_limit.rs` | Use `path_matches_skip()`; fix `window_start` reset |
|
|
120
|
+
| `src/middleware/guards.rs` | Use `path_matches_skip()` |
|
|
121
|
+
| `src/minijinja_engine.rs` | Remove unused `mut` from test bindings |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Upgrade
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install --upgrade cello-framework==1.2.2
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Drop-in replacement for v1.2.1. No API changes.
|
|
@@ -143,6 +143,9 @@ from cello._cello import (
|
|
|
143
143
|
HttpResponse,
|
|
144
144
|
)
|
|
145
145
|
|
|
146
|
+
# RFC 7807 Problem Details
|
|
147
|
+
from cello._cello import ProblemDetails
|
|
148
|
+
|
|
146
149
|
def validate_jwt_config(config: JwtConfig) -> JwtConfig:
|
|
147
150
|
"""Validate a JwtConfig instance.
|
|
148
151
|
|
|
@@ -285,13 +288,15 @@ __all__ = [
|
|
|
285
288
|
"EventSourcingConfig",
|
|
286
289
|
"CqrsConfig",
|
|
287
290
|
"SagaConfig",
|
|
291
|
+
# RFC 7807
|
|
292
|
+
"ProblemDetails",
|
|
288
293
|
# Config validators
|
|
289
294
|
"validate_jwt_config",
|
|
290
295
|
"validate_session_config",
|
|
291
296
|
"validate_rate_limit_config",
|
|
292
297
|
"validate_tls_config",
|
|
293
298
|
]
|
|
294
|
-
__version__ = "1.2.
|
|
299
|
+
__version__ = "1.2.2"
|
|
295
300
|
|
|
296
301
|
|
|
297
302
|
class Blueprint:
|
|
@@ -124,9 +124,12 @@ class Authenticated(Guard):
|
|
|
124
124
|
return True
|
|
125
125
|
|
|
126
126
|
class And(Guard):
|
|
127
|
-
"""Pass only if ALL guards pass."""
|
|
128
|
-
def __init__(self,
|
|
129
|
-
|
|
127
|
+
"""Pass only if ALL guards pass. Accepts a list or *args: And([g1, g2]) or And(g1, g2)."""
|
|
128
|
+
def __init__(self, *args):
|
|
129
|
+
if len(args) == 1 and isinstance(args[0], list):
|
|
130
|
+
self.guards = args[0]
|
|
131
|
+
else:
|
|
132
|
+
self.guards = list(args)
|
|
130
133
|
|
|
131
134
|
def __call__(self, request: Any):
|
|
132
135
|
for guard in self.guards:
|
|
@@ -139,9 +142,12 @@ class And(Guard):
|
|
|
139
142
|
return True
|
|
140
143
|
|
|
141
144
|
class Or(Guard):
|
|
142
|
-
"""Pass if ANY guard passes."""
|
|
143
|
-
def __init__(self,
|
|
144
|
-
|
|
145
|
+
"""Pass if ANY guard passes. Accepts a list or *args: Or([g1, g2]) or Or(g1, g2)."""
|
|
146
|
+
def __init__(self, *args):
|
|
147
|
+
if len(args) == 1 and isinstance(args[0], list):
|
|
148
|
+
self.guards = args[0]
|
|
149
|
+
else:
|
|
150
|
+
self.guards = list(args)
|
|
145
151
|
|
|
146
152
|
def __call__(self, request: Any):
|
|
147
153
|
last_error = None
|
|
@@ -754,60 +754,55 @@ def openapi_handler(request):
|
|
|
754
754
|
let startup_handlers = self.startup_handlers.clone();
|
|
755
755
|
let shutdown_handlers = self.shutdown_handlers.clone();
|
|
756
756
|
|
|
757
|
-
//
|
|
757
|
+
// Release the GIL and run a native Tokio current-thread runtime.
|
|
758
758
|
//
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
for handler in &startup_handlers {
|
|
792
|
-
if let Err(e) = run_lifecycle_handler_async(handler.clone()).await {
|
|
793
|
-
eprintln!("Error in startup handler: {e}");
|
|
794
|
-
}
|
|
795
|
-
}
|
|
759
|
+
// pyo3_asyncio::tokio::run was previously used here but it drives Tokio I/O
|
|
760
|
+
// through Python's asyncio selector loop, which breaks socket binding in
|
|
761
|
+
// environments where the two event loops don't integrate (Python 3.12+ / pyo3 0.20).
|
|
762
|
+
// Since the server's hot path is pure Rust I/O, we release the GIL with
|
|
763
|
+
// allow_threads and block on a self-contained Tokio runtime. Python handlers
|
|
764
|
+
// re-acquire the GIL individually via Python::with_gil when they need it.
|
|
765
|
+
py.allow_threads(|| {
|
|
766
|
+
tokio::runtime::Builder::new_current_thread()
|
|
767
|
+
.enable_all()
|
|
768
|
+
.build()
|
|
769
|
+
.expect("failed to build Tokio runtime")
|
|
770
|
+
.block_on(async move {
|
|
771
|
+
let mut config = server::ServerConfig::new(&host_owned, port);
|
|
772
|
+
config.workers = workers.unwrap_or(0);
|
|
773
|
+
|
|
774
|
+
let server = Server::new(
|
|
775
|
+
config,
|
|
776
|
+
router,
|
|
777
|
+
handlers,
|
|
778
|
+
middleware,
|
|
779
|
+
websocket_handlers,
|
|
780
|
+
dependency_container,
|
|
781
|
+
guards,
|
|
782
|
+
prometheus,
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Startup hooks
|
|
786
|
+
for handler in &startup_handlers {
|
|
787
|
+
if let Err(e) = run_lifecycle_handler_async(handler.clone()).await {
|
|
788
|
+
eprintln!("Error in startup handler: {e}");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
796
791
|
|
|
797
|
-
|
|
792
|
+
let _ = server.run().await;
|
|
798
793
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
794
|
+
// Shutdown hooks
|
|
795
|
+
for handler in &shutdown_handlers {
|
|
796
|
+
match run_lifecycle_handler_async(handler.clone()).await {
|
|
797
|
+
Err(e) if !e.to_string().contains("KeyboardInterrupt") => {
|
|
798
|
+
eprintln!("Error in shutdown handler: {e}");
|
|
799
|
+
}
|
|
800
|
+
_ => {}
|
|
801
|
+
}
|
|
804
802
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
Ok(())
|
|
810
|
-
})
|
|
803
|
+
})
|
|
804
|
+
});
|
|
805
|
+
Ok(())
|
|
811
806
|
}
|
|
812
807
|
|
|
813
808
|
/// Internal route registration.
|
|
@@ -2112,5 +2107,8 @@ fn _cello(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
|
|
2112
2107
|
m.add_class::<PyCqrsConfig>()?;
|
|
2113
2108
|
m.add_class::<PySagaConfig>()?;
|
|
2114
2109
|
|
|
2110
|
+
// RFC 7807 error type
|
|
2111
|
+
m.add_class::<error::ProblemDetails>()?;
|
|
2112
|
+
|
|
2115
2113
|
Ok(())
|
|
2116
2114
|
}
|