tina4-python 3.12.14__tar.gz → 3.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tina4_python-3.12.14 → tina4_python-3.13.0}/PKG-INFO +3 -3
- {tina4_python-3.12.14 → tina4_python-3.13.0}/README.md +2 -2
- {tina4_python-3.12.14 → tina4_python-3.13.0}/pyproject.toml +1 -1
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/CLAUDE.md +4 -4
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/__init__.py +24 -1
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/ai/__init__.py +74 -7
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/api/__init__.py +46 -4
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/auth/__init__.py +43 -15
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/cli/__init__.py +12 -6
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/container/__init__.py +21 -2
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/response.py +70 -17
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/router.py +85 -1
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/adapter.py +17 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/connection.py +43 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/frond/engine.py +54 -16
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/graphql/__init__.py +91 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/i18n/__init__.py +42 -5
- tina4_python-3.13.0/tina4_python/migration/__init__.py +47 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/orm/model.py +58 -9
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/__init__.py +55 -11
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/job.py +6 -1
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/session/__init__.py +31 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/swagger/__init__.py +76 -13
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/test/__init__.py +42 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/websocket/__init__.py +16 -0
- tina4_python-3.12.14/tina4_python/migration/__init__.py +0 -15
- {tina4_python-3.12.14 → tina4_python-3.13.0}/.gitignore +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/Testing.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/events.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/request.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/core/server.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/docs.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.12.14 → tina4_python-3.13.0}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.13.0
|
|
4
4
|
Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -452,7 +452,7 @@ request.session.set("user_id", 42)
|
|
|
452
452
|
user_id = request.session.get("user_id")
|
|
453
453
|
```
|
|
454
454
|
|
|
455
|
-
Backends: file (default), Redis, Valkey, MongoDB, database. Set via `
|
|
455
|
+
Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_BACKEND` in `.env`.
|
|
456
456
|
|
|
457
457
|
### Queues
|
|
458
458
|
|
|
@@ -699,7 +699,7 @@ TINA4_DATABASE_URL=sqlite:///data/app.db
|
|
|
699
699
|
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
700
700
|
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
701
701
|
TINA4_LOCALE=en # en, fr, af, zh, ja, es
|
|
702
|
-
|
|
702
|
+
TINA4_SESSION_BACKEND=SessionFileHandler
|
|
703
703
|
SWAGGER_TITLE=My API
|
|
704
704
|
```
|
|
705
705
|
|
|
@@ -420,7 +420,7 @@ request.session.set("user_id", 42)
|
|
|
420
420
|
user_id = request.session.get("user_id")
|
|
421
421
|
```
|
|
422
422
|
|
|
423
|
-
Backends: file (default), Redis, Valkey, MongoDB, database. Set via `
|
|
423
|
+
Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_BACKEND` in `.env`.
|
|
424
424
|
|
|
425
425
|
### Queues
|
|
426
426
|
|
|
@@ -667,7 +667,7 @@ TINA4_DATABASE_URL=sqlite:///data/app.db
|
|
|
667
667
|
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
668
668
|
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
669
669
|
TINA4_LOCALE=en # en, fr, af, zh, ja, es
|
|
670
|
-
|
|
670
|
+
TINA4_SESSION_BACKEND=SessionFileHandler
|
|
671
671
|
SWAGGER_TITLE=My API
|
|
672
672
|
```
|
|
673
673
|
|
|
@@ -498,7 +498,7 @@ TINA4_TOKEN_LIMIT is used to set the session time, default 60 minutes
|
|
|
498
498
|
|
|
499
499
|
### Session Backends
|
|
500
500
|
|
|
501
|
-
Set `
|
|
501
|
+
Set `TINA4_SESSION_BACKEND` to choose a backend:
|
|
502
502
|
|
|
503
503
|
| Handler | Backend | Required package |
|
|
504
504
|
|---------|---------|-----------------|
|
|
@@ -510,7 +510,7 @@ Set `TINA4_SESSION_HANDLER` to choose a backend:
|
|
|
510
510
|
#### MongoDB session env vars
|
|
511
511
|
|
|
512
512
|
```bash
|
|
513
|
-
|
|
513
|
+
TINA4_SESSION_BACKEND=SessionMongoHandler
|
|
514
514
|
TINA4_SESSION_MONGO_HOST=localhost # default
|
|
515
515
|
TINA4_SESSION_MONGO_PORT=27017 # default
|
|
516
516
|
TINA4_SESSION_MONGO_URI= # full URI (overrides host/port)
|
|
@@ -1411,7 +1411,7 @@ async def products(request, response):
|
|
|
1411
1411
|
return response(expensive_query())
|
|
1412
1412
|
|
|
1413
1413
|
# Per-route TTL override via @cached decorator
|
|
1414
|
-
@cached(
|
|
1414
|
+
@cached(max_age=120)
|
|
1415
1415
|
@get("/api/slow")
|
|
1416
1416
|
async def slow(request, response):
|
|
1417
1417
|
return response(very_slow_query())
|
|
@@ -1578,7 +1578,7 @@ TINA4_OVERRIDE_CLIENT=false # Set to true to allow running without tina4 C
|
|
|
1578
1578
|
HOST_NAME=localhost:7145
|
|
1579
1579
|
|
|
1580
1580
|
# Sessions
|
|
1581
|
-
|
|
1581
|
+
TINA4_SESSION_BACKEND=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
|
|
1582
1582
|
TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
|
|
1583
1583
|
|
|
1584
1584
|
# Swagger/OpenAPI
|
|
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
|
|
|
8
8
|
|
|
9
9
|
One import, everything works.
|
|
10
10
|
"""
|
|
11
|
-
__version__ = "3.
|
|
11
|
+
__version__ = "3.13.0"
|
|
12
12
|
|
|
13
13
|
# ── Route decorators ──
|
|
14
14
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
@@ -61,3 +61,26 @@ from tina4_python.container import Container # noqa: E402, F401
|
|
|
61
61
|
|
|
62
62
|
# ── Server ──
|
|
63
63
|
from tina4_python.core.server import run, background # noqa: E402, F401
|
|
64
|
+
|
|
65
|
+
# ── HTTP Client ──
|
|
66
|
+
from tina4_python.api import Api # noqa: E402, F401
|
|
67
|
+
|
|
68
|
+
# ── SOAP / WSDL ──
|
|
69
|
+
from tina4_python.wsdl import WSDL, wsdl_operation # noqa: E402, F401
|
|
70
|
+
|
|
71
|
+
# ── GraphQL ──
|
|
72
|
+
from tina4_python.graphql import GraphQL # noqa: E402, F401
|
|
73
|
+
|
|
74
|
+
# ── Auto-CRUD scaffolder ──
|
|
75
|
+
from tina4_python.crud import AutoCrud # noqa: E402, F401
|
|
76
|
+
|
|
77
|
+
# ── Events (decoupled communication) ──
|
|
78
|
+
from tina4_python.core.events import on, emit, once, off # noqa: E402, F401
|
|
79
|
+
|
|
80
|
+
# ── Email (Messenger) ──
|
|
81
|
+
from tina4_python.messenger import Messenger # noqa: E402, F401
|
|
82
|
+
|
|
83
|
+
# ── Inline testing (@tests + assertions for inline test cases) ──
|
|
84
|
+
# Class-based xUnit testing lives in tina4_python.test (a separate module).
|
|
85
|
+
# Keep both surfaces re-exported so users can write either style.
|
|
86
|
+
from tina4_python.Testing import tests, assert_equal as assert_equal_inline # noqa: E402, F401
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
# Tina4 AI —
|
|
1
|
+
# Tina4 AI — Detect and install AI coding assistant context files.
|
|
2
2
|
"""
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Detect which AI tools are present in a project and install Tina4-aware
|
|
4
|
+
context files so any assistant understands the framework.
|
|
5
5
|
|
|
6
|
-
from tina4_python.ai import
|
|
6
|
+
from tina4_python.ai import detect_ai, install_context, status_report
|
|
7
|
+
|
|
8
|
+
tools = detect_ai(".") # → [{"name": "claude-code", "installed": True, ...}, ...]
|
|
9
|
+
install_context(".") # install for all known tools
|
|
10
|
+
install_context(".", tools=["claude-code", "cursor"])
|
|
11
|
+
print(status_report(".")) # human-readable summary
|
|
7
12
|
"""
|
|
8
13
|
import os
|
|
9
14
|
import shutil
|
|
@@ -29,6 +34,47 @@ def is_installed(root: str, tool: dict) -> bool:
|
|
|
29
34
|
return (Path(root).resolve() / tool["context_file"]).exists()
|
|
30
35
|
|
|
31
36
|
|
|
37
|
+
# ── Detection API (docs-friendly names) ────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detect_ai(root: str = ".") -> list[dict]:
|
|
41
|
+
"""Detect which AI coding tools are installed in ``root``.
|
|
42
|
+
|
|
43
|
+
Returns a list of tool dicts, one per known AI tool, with an
|
|
44
|
+
``installed`` boolean added. The list mirrors ``AI_TOOLS`` order.
|
|
45
|
+
|
|
46
|
+
from tina4_python.ai import detect_ai
|
|
47
|
+
tools = detect_ai(".")
|
|
48
|
+
installed = [t for t in tools if t["installed"]]
|
|
49
|
+
"""
|
|
50
|
+
return [{**t, "installed": is_installed(root, t)} for t in AI_TOOLS]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def detect_ai_names(root: str = ".") -> list[str]:
|
|
54
|
+
"""Return only the names of detected (installed) AI tools.
|
|
55
|
+
|
|
56
|
+
names = detect_ai_names(".") # → ["claude-code", "cursor"]
|
|
57
|
+
"""
|
|
58
|
+
return [t["name"] for t in AI_TOOLS if is_installed(root, t)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def status_report(root: str = ".") -> str:
|
|
62
|
+
"""Human-readable summary of AI-tool installation state in ``root``.
|
|
63
|
+
|
|
64
|
+
Returns a multi-line string suitable for printing — lists each tool
|
|
65
|
+
along with installed/not-installed and the path of the context file.
|
|
66
|
+
"""
|
|
67
|
+
root = str(Path(root).resolve())
|
|
68
|
+
lines = [f"AI tool status — {root}", ""]
|
|
69
|
+
for tool in AI_TOOLS:
|
|
70
|
+
marker = "✓ installed" if is_installed(root, tool) else " not present"
|
|
71
|
+
lines.append(f" {marker} {tool['description']:<24s} → {tool['context_file']}")
|
|
72
|
+
installed_count = sum(1 for t in AI_TOOLS if is_installed(root, t))
|
|
73
|
+
lines.append("")
|
|
74
|
+
lines.append(f" {installed_count}/{len(AI_TOOLS)} tools have Tina4 context.")
|
|
75
|
+
return "\n".join(lines)
|
|
76
|
+
|
|
77
|
+
|
|
32
78
|
def show_menu(root: str = ".") -> str:
|
|
33
79
|
"""Print the numbered menu and return user input."""
|
|
34
80
|
root = str(Path(root).resolve())
|
|
@@ -89,9 +135,30 @@ def install_selected(root: str, selection: str) -> list[str]:
|
|
|
89
135
|
return created
|
|
90
136
|
|
|
91
137
|
|
|
92
|
-
def
|
|
93
|
-
"""Install context for
|
|
94
|
-
|
|
138
|
+
def install_context(root: str = ".", tools: "list[str] | None" = None) -> list[str]:
|
|
139
|
+
"""Install Tina4 context files for AI tools (non-interactive).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
root: Project root directory.
|
|
143
|
+
tools: Optional list of tool names. When omitted, installs for all
|
|
144
|
+
known AI tools.
|
|
145
|
+
|
|
146
|
+
Returns a list of relative paths to the files that were created.
|
|
147
|
+
|
|
148
|
+
install_context(".") # all tools
|
|
149
|
+
install_context(".", tools=["claude-code"]) # just one
|
|
150
|
+
"""
|
|
151
|
+
if tools is None:
|
|
152
|
+
selection = "all"
|
|
153
|
+
else:
|
|
154
|
+
# Translate names → indices for install_selected's CSV input
|
|
155
|
+
index_map = {t["name"]: str(i + 1) for i, t in enumerate(AI_TOOLS)}
|
|
156
|
+
try:
|
|
157
|
+
selection = ",".join(index_map[name] for name in tools)
|
|
158
|
+
except KeyError as e:
|
|
159
|
+
known = ", ".join(t["name"] for t in AI_TOOLS)
|
|
160
|
+
raise ValueError(f"Unknown AI tool: {e!s}. Known: {known}") from e
|
|
161
|
+
return install_selected(root, selection)
|
|
95
162
|
|
|
96
163
|
|
|
97
164
|
def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
|
|
@@ -20,13 +20,50 @@ class Api:
|
|
|
20
20
|
"""HTTP client using urllib — zero external dependencies."""
|
|
21
21
|
|
|
22
22
|
def __init__(self, base_url: str = "", auth_header: str = "",
|
|
23
|
-
ignore_ssl: bool = False, timeout: int = 30
|
|
23
|
+
ignore_ssl: bool = False, timeout: int = 30,
|
|
24
|
+
bearer_token: str | None = None,
|
|
25
|
+
username: str | None = None,
|
|
26
|
+
password: str | None = None,
|
|
27
|
+
headers: dict[str, str] | None = None,
|
|
28
|
+
verify_ssl: bool | None = None):
|
|
29
|
+
"""HTTP client.
|
|
30
|
+
|
|
31
|
+
Constructor accepts ergonomic kwargs the documentation has long
|
|
32
|
+
described — every modern Python HTTP library (requests, httpx)
|
|
33
|
+
accepts these directly rather than requiring post-construction
|
|
34
|
+
setter calls.
|
|
35
|
+
|
|
36
|
+
api = Api("https://api.example.com", bearer_token="sk-...")
|
|
37
|
+
api = Api("https://api.example.com", username="u", password="p")
|
|
38
|
+
api = Api("https://api.example.com", headers={"X-Tenant": "acme"})
|
|
39
|
+
api = Api("https://self-signed.local", verify_ssl=False)
|
|
40
|
+
|
|
41
|
+
The setter-based API (``set_bearer_token``, ``set_basic_auth``,
|
|
42
|
+
``add_headers``) continues to work; pick whichever reads better.
|
|
43
|
+
|
|
44
|
+
``verify_ssl`` is the docs-friendly inverse of ``ignore_ssl`` —
|
|
45
|
+
``verify_ssl=False`` is equivalent to ``ignore_ssl=True``. If
|
|
46
|
+
both are supplied, ``ignore_ssl`` wins (legacy precedence).
|
|
47
|
+
"""
|
|
24
48
|
self.base_url = base_url.rstrip("/")
|
|
25
49
|
self.auth_header = auth_header
|
|
26
50
|
self.timeout = timeout
|
|
27
51
|
self._headers: dict[str, str] = {}
|
|
28
52
|
self._ssl_context = None
|
|
29
|
-
|
|
53
|
+
|
|
54
|
+
# ── kwarg sugar ────────────────────────────────────────────────
|
|
55
|
+
# Bearer token wins over basic auth if both are passed.
|
|
56
|
+
if bearer_token is not None:
|
|
57
|
+
self.set_bearer_token(bearer_token)
|
|
58
|
+
elif username is not None and password is not None:
|
|
59
|
+
self.set_basic_auth(username, password)
|
|
60
|
+
|
|
61
|
+
if headers:
|
|
62
|
+
self._headers.update(headers)
|
|
63
|
+
|
|
64
|
+
# ignore_ssl is the existing flag; verify_ssl=False is the same thing
|
|
65
|
+
# expressed positively. Honour ignore_ssl when both are set.
|
|
66
|
+
if ignore_ssl or (verify_ssl is False):
|
|
30
67
|
self._ssl_context = ssl.create_default_context()
|
|
31
68
|
self._ssl_context.check_hostname = False
|
|
32
69
|
self._ssl_context.verify_mode = ssl.CERT_NONE
|
|
@@ -68,9 +105,14 @@ class Api:
|
|
|
68
105
|
"""HTTP DELETE request."""
|
|
69
106
|
return self._request("DELETE", self._url(path), body)
|
|
70
107
|
|
|
71
|
-
def
|
|
108
|
+
def send(self, method: str, path: str = "", body=None,
|
|
72
109
|
content_type: str = "application/json") -> dict:
|
|
73
|
-
"""Generic request method.
|
|
110
|
+
"""Generic request method — pick HTTP verb at call time.
|
|
111
|
+
|
|
112
|
+
Renamed from ``send_request`` in 3.13.0 for parity with the
|
|
113
|
+
documentation and conciseness (``api.send("PATCH", ...)`` reads
|
|
114
|
+
cleaner than ``api.send_request("PATCH", ...)``).
|
|
115
|
+
"""
|
|
74
116
|
return self._request(method.upper(), self._url(path), body, content_type)
|
|
75
117
|
|
|
76
118
|
def _url(self, path: str) -> str:
|
|
@@ -62,8 +62,14 @@ class Auth:
|
|
|
62
62
|
"""
|
|
63
63
|
self.secret = secret or os.environ.get("TINA4_SECRET", "tina4-default-secret")
|
|
64
64
|
self.algorithm = algorithm
|
|
65
|
+
# JWT expiry env var:
|
|
66
|
+
# - TINA4_TOKEN_EXPIRES_IN (preferred — matches docs and the
|
|
67
|
+
# form-token expiry env var used by Frond)
|
|
68
|
+
# - TINA4_TOKEN_LIMIT (legacy — accepted for backward compat)
|
|
69
|
+
# Constructor arg wins over both env vars.
|
|
65
70
|
self.expires_in = expires_in or int(
|
|
66
|
-
os.environ.get("
|
|
71
|
+
os.environ.get("TINA4_TOKEN_EXPIRES_IN")
|
|
72
|
+
or os.environ.get("TINA4_TOKEN_LIMIT", "60")
|
|
67
73
|
)
|
|
68
74
|
|
|
69
75
|
# ── JWT ────────────────────────────────────────────────────────
|
|
@@ -98,26 +104,42 @@ class Auth:
|
|
|
98
104
|
return f"{h}.{p}.{signature}"
|
|
99
105
|
|
|
100
106
|
@_DualMethod
|
|
101
|
-
def valid_token(self, token: str) ->
|
|
102
|
-
"""Validate a JWT signature
|
|
107
|
+
def valid_token(self, token: str) -> "dict | None":
|
|
108
|
+
"""Validate a JWT signature + expiry. Returns the decoded payload
|
|
109
|
+
on success, or ``None`` if invalid / expired / malformed.
|
|
110
|
+
|
|
111
|
+
The payload dict is truthy for valid tokens and ``None`` is falsy,
|
|
112
|
+
so the legacy ``if Auth.valid_token(t):`` boolean-style usage keeps
|
|
113
|
+
working unchanged. Callers that want the payload can now read it
|
|
114
|
+
directly instead of a separate ``get_payload(t)`` call::
|
|
115
|
+
|
|
116
|
+
payload = Auth.valid_token(token)
|
|
117
|
+
if payload is None:
|
|
118
|
+
return response("Unauthorized", 401)
|
|
119
|
+
user_id = payload["user_id"]
|
|
120
|
+
|
|
121
|
+
Matches the convention used by PyJWT, python-jose, and authlib —
|
|
122
|
+
validity check returns the payload because the validation IS the
|
|
123
|
+
payload decode. Returning bare True/False discarded that information.
|
|
124
|
+
"""
|
|
103
125
|
try:
|
|
104
126
|
parts = token.split(".")
|
|
105
127
|
if len(parts) != 3:
|
|
106
|
-
return
|
|
128
|
+
return None
|
|
107
129
|
|
|
108
130
|
h, p, sig = parts
|
|
109
131
|
expected = self._sign(f"{h}.{p}")
|
|
110
132
|
if not hmac.compare_digest(sig, expected):
|
|
111
|
-
return
|
|
133
|
+
return None
|
|
112
134
|
|
|
113
135
|
payload = json.loads(_b64url_decode(p))
|
|
114
136
|
|
|
115
137
|
if "exp" in payload and time.time() > payload["exp"]:
|
|
116
|
-
return
|
|
138
|
+
return None
|
|
117
139
|
|
|
118
|
-
return
|
|
140
|
+
return payload
|
|
119
141
|
except Exception:
|
|
120
|
-
return
|
|
142
|
+
return None
|
|
121
143
|
|
|
122
144
|
@_DualMethod
|
|
123
145
|
def get_payload(self, token: str) -> dict | None:
|
|
@@ -137,9 +159,7 @@ class Auth:
|
|
|
137
159
|
Args:
|
|
138
160
|
expires_in: Lifetime in minutes (default: self.expires_in).
|
|
139
161
|
"""
|
|
140
|
-
|
|
141
|
-
return None
|
|
142
|
-
payload = self.get_payload(token)
|
|
162
|
+
payload = self.valid_token(token)
|
|
143
163
|
if payload is None:
|
|
144
164
|
return None
|
|
145
165
|
payload.pop("iat", None)
|
|
@@ -166,8 +186,11 @@ class Auth:
|
|
|
166
186
|
return auth.get_token(payload)
|
|
167
187
|
|
|
168
188
|
@classmethod
|
|
169
|
-
def valid_token_static(cls, token: str) ->
|
|
170
|
-
"""Validate a JWT without instantiating Auth — reads SECRET from env.
|
|
189
|
+
def valid_token_static(cls, token: str) -> "dict | None":
|
|
190
|
+
"""Validate a JWT without instantiating Auth — reads SECRET from env.
|
|
191
|
+
|
|
192
|
+
Returns the decoded payload on success, ``None`` on failure.
|
|
193
|
+
"""
|
|
171
194
|
secret = os.environ.get("TINA4_SECRET", "tina4-default-secret")
|
|
172
195
|
auth = cls(secret=secret)
|
|
173
196
|
return auth.valid_token(token)
|
|
@@ -307,8 +330,13 @@ def get_token(payload: dict, expires_in: int = 60, secret: str = None) -> str:
|
|
|
307
330
|
return Auth.get_token_static(payload, expires_in=expires_in)
|
|
308
331
|
|
|
309
332
|
|
|
310
|
-
def valid_token(token: str) ->
|
|
311
|
-
"""Validate a JWT signature and expiry — reads SECRET from env.
|
|
333
|
+
def valid_token(token: str) -> "dict | None":
|
|
334
|
+
"""Validate a JWT signature and expiry — reads SECRET from env.
|
|
335
|
+
|
|
336
|
+
Returns the decoded payload dict on success, ``None`` on failure.
|
|
337
|
+
The truthy/falsy split keeps legacy ``if valid_token(t):`` code working
|
|
338
|
+
while letting new code read the payload directly.
|
|
339
|
+
"""
|
|
312
340
|
return Auth.valid_token_static(token)
|
|
313
341
|
|
|
314
342
|
|
|
@@ -399,9 +399,9 @@ def _init(args):
|
|
|
399
399
|
)
|
|
400
400
|
|
|
401
401
|
# AI context
|
|
402
|
-
from tina4_python.ai import
|
|
402
|
+
from tina4_python.ai import install_context
|
|
403
403
|
if "--ai" in args:
|
|
404
|
-
created =
|
|
404
|
+
created = install_context(str(target))
|
|
405
405
|
if created:
|
|
406
406
|
print("\nAI context installed for all supported tools:")
|
|
407
407
|
for f in created:
|
|
@@ -602,10 +602,10 @@ def _build(args):
|
|
|
602
602
|
|
|
603
603
|
def _ai(args):
|
|
604
604
|
"""Install AI coding assistant context files."""
|
|
605
|
-
from tina4_python.ai import show_menu, install_selected,
|
|
605
|
+
from tina4_python.ai import show_menu, install_selected, install_context
|
|
606
606
|
|
|
607
607
|
if args and args[0].lower() == "all":
|
|
608
|
-
|
|
608
|
+
install_context(".")
|
|
609
609
|
else:
|
|
610
610
|
selection = show_menu(".")
|
|
611
611
|
if selection:
|
|
@@ -743,8 +743,14 @@ async def list_{route_path}(request, response):
|
|
|
743
743
|
page = int(request.params.get("page", 1))
|
|
744
744
|
per_page = int(request.params.get("per_page", 20))
|
|
745
745
|
offset = (page - 1) * per_page
|
|
746
|
-
|
|
747
|
-
return response(
|
|
746
|
+
records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
|
|
747
|
+
return response({{
|
|
748
|
+
"records": [r.to_dict() for r in records],
|
|
749
|
+
"count": total,
|
|
750
|
+
"page": page,
|
|
751
|
+
"per_page": per_page,
|
|
752
|
+
"total_pages": max(1, -(-total // per_page)),
|
|
753
|
+
}})
|
|
748
754
|
|
|
749
755
|
|
|
750
756
|
@noauth()
|
|
@@ -27,7 +27,8 @@ class Container:
|
|
|
27
27
|
subsequent calls return the memoised instance.
|
|
28
28
|
- ``get(name)`` — resolve a dependency by name.
|
|
29
29
|
- ``has(name)`` — check if a name is registered.
|
|
30
|
-
- ``reset()`` — clear
|
|
30
|
+
- ``reset()`` — clear cached singletons; keep factories.
|
|
31
|
+
- ``reset_all()`` — clear both singletons AND factories.
|
|
31
32
|
"""
|
|
32
33
|
|
|
33
34
|
def __init__(self):
|
|
@@ -80,6 +81,24 @@ class Container:
|
|
|
80
81
|
return name in self._factories
|
|
81
82
|
|
|
82
83
|
def reset(self) -> None:
|
|
83
|
-
"""Clear
|
|
84
|
+
"""Clear cached singleton instances; keep factory registrations.
|
|
85
|
+
|
|
86
|
+
Common test-fixture pattern: reset to drop cached state between
|
|
87
|
+
tests while keeping the DI graph wired. Transient registrations
|
|
88
|
+
are unaffected (they have no cache to clear).
|
|
89
|
+
|
|
90
|
+
For the old behaviour (clear everything including factories) use
|
|
91
|
+
``reset_all()``.
|
|
92
|
+
"""
|
|
93
|
+
with self._lock:
|
|
94
|
+
for entry in self._factories.values():
|
|
95
|
+
if entry.get("singleton"):
|
|
96
|
+
entry["instance"] = None
|
|
97
|
+
|
|
98
|
+
def reset_all(self) -> None:
|
|
99
|
+
"""Clear ALL registrations — both factories and cached singletons.
|
|
100
|
+
|
|
101
|
+
Tear-down for end-of-suite / process-shutdown scenarios.
|
|
102
|
+
"""
|
|
84
103
|
with self._lock:
|
|
85
104
|
self._factories.clear()
|
|
@@ -87,18 +87,26 @@ class Response:
|
|
|
87
87
|
self._is_streaming: bool = False
|
|
88
88
|
self._stream_source = None
|
|
89
89
|
|
|
90
|
-
def __call__(self, data=None, status_code: int = 200, content_type: str = None
|
|
90
|
+
def __call__(self, data=None, status_code: int = 200, content_type: str = None,
|
|
91
|
+
headers: dict | None = None) -> "Response":
|
|
91
92
|
"""Smart callable — auto-detects content type from data.
|
|
92
93
|
|
|
93
94
|
Usage:
|
|
94
|
-
return response({"key": "value"})
|
|
95
|
-
return response({"ok": True}, HTTP_CREATED)
|
|
96
|
-
return response("<h1>Hello</h1>")
|
|
97
|
-
return response("plain text", HTTP_OK)
|
|
98
|
-
return response(data, HTTP_OK, APPLICATION_JSON)
|
|
95
|
+
return response({"key": "value"}) # JSON
|
|
96
|
+
return response({"ok": True}, HTTP_CREATED) # JSON with status
|
|
97
|
+
return response("<h1>Hello</h1>") # HTML
|
|
98
|
+
return response("plain text", HTTP_OK) # Plain text
|
|
99
|
+
return response(data, HTTP_OK, APPLICATION_JSON) # Explicit
|
|
100
|
+
return response(data, headers={"X-Tenant": "acme"}) # One-shot headers
|
|
99
101
|
"""
|
|
100
102
|
self.status_code = status_code
|
|
101
103
|
|
|
104
|
+
# Optional one-shot headers — equivalent to chaining .header(k, v)
|
|
105
|
+
# for each entry, but lets call sites stay on a single expression.
|
|
106
|
+
if headers:
|
|
107
|
+
for k, v in headers.items():
|
|
108
|
+
self._headers.append((k, v))
|
|
109
|
+
|
|
102
110
|
if content_type:
|
|
103
111
|
# Explicit content type provided
|
|
104
112
|
self.content_type = content_type
|
|
@@ -149,15 +157,48 @@ class Response:
|
|
|
149
157
|
"""Add a response header (chainable). Alias for header()."""
|
|
150
158
|
return self.header(name, value)
|
|
151
159
|
|
|
152
|
-
def cookie(self, name: str, value: str,
|
|
153
|
-
max_age: int =
|
|
154
|
-
secure: bool =
|
|
155
|
-
"""Set a cookie (chainable).
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
def cookie(self, name: str, value: str, options=None, *,
|
|
161
|
+
path: str = None, max_age: int = None, http_only: bool = None,
|
|
162
|
+
secure: bool = None, same_site: str = None) -> "Response":
|
|
163
|
+
"""Set a cookie (chainable). Two equivalent forms:
|
|
164
|
+
|
|
165
|
+
# Kwarg form (original)
|
|
166
|
+
response.cookie("session", token, max_age=3600, http_only=True)
|
|
167
|
+
|
|
168
|
+
# Dict-options form (when config comes from a settings object)
|
|
169
|
+
COOKIE_OPTS = {"max_age": 3600, "http_only": True, "secure": True}
|
|
170
|
+
response.cookie("session", token, COOKIE_OPTS)
|
|
171
|
+
|
|
172
|
+
When ``options`` is a dict, its values become the defaults; any
|
|
173
|
+
explicit kwarg passed afterwards overrides individual entries.
|
|
174
|
+
"""
|
|
175
|
+
# Defaults
|
|
176
|
+
_path = "/"
|
|
177
|
+
_max_age = 3600
|
|
178
|
+
_http_only = True
|
|
179
|
+
_secure = False
|
|
180
|
+
_same_site = "Lax"
|
|
181
|
+
|
|
182
|
+
# Dict-options form
|
|
183
|
+
if isinstance(options, dict):
|
|
184
|
+
_path = options.get("path", _path)
|
|
185
|
+
_max_age = options.get("max_age", _max_age)
|
|
186
|
+
_http_only = options.get("http_only", _http_only)
|
|
187
|
+
_secure = options.get("secure", _secure)
|
|
188
|
+
_same_site = options.get("same_site", _same_site)
|
|
189
|
+
|
|
190
|
+
# Explicit kwargs win over dict
|
|
191
|
+
if path is not None: _path = path
|
|
192
|
+
if max_age is not None: _max_age = max_age
|
|
193
|
+
if http_only is not None: _http_only = http_only
|
|
194
|
+
if secure is not None: _secure = secure
|
|
195
|
+
if same_site is not None: _same_site = same_site
|
|
196
|
+
|
|
197
|
+
parts = [f"{name}={value}", f"Path={_path}", f"Max-Age={_max_age}",
|
|
198
|
+
f"SameSite={_same_site}"]
|
|
199
|
+
if _http_only:
|
|
159
200
|
parts.append("HttpOnly")
|
|
160
|
-
if
|
|
201
|
+
if _secure:
|
|
161
202
|
parts.append("Secure")
|
|
162
203
|
self._cookies.append("; ".join(parts))
|
|
163
204
|
return self
|
|
@@ -252,19 +293,28 @@ class Response:
|
|
|
252
293
|
)
|
|
253
294
|
return self
|
|
254
295
|
|
|
255
|
-
def render(self, template: str, data: dict = None) -> "Response":
|
|
296
|
+
def render(self, template: str, data: dict = None, status_code: int = None) -> "Response":
|
|
256
297
|
"""Render a Frond/Twig template with data.
|
|
257
298
|
|
|
258
299
|
Uses the global Frond engine (registered via set_frond()) so that
|
|
259
300
|
custom filters and globals are available in all templates.
|
|
260
301
|
Falls back to framework templates if not found in user dir.
|
|
302
|
+
|
|
303
|
+
The optional ``status_code`` lets error-page handlers render the
|
|
304
|
+
page and set the response status in one call::
|
|
305
|
+
|
|
306
|
+
return response.render("errors/404.twig", {}, 404)
|
|
307
|
+
return response.render("errors/500.twig", {"err": str(e)}, 500)
|
|
261
308
|
"""
|
|
262
309
|
engine = get_frond()
|
|
263
310
|
|
|
264
311
|
# Try user templates first (the global engine's directory)
|
|
265
312
|
try:
|
|
266
313
|
html = engine.render(template, data or {})
|
|
267
|
-
|
|
314
|
+
rendered = self.html(html)
|
|
315
|
+
if status_code is not None:
|
|
316
|
+
rendered.status_code = status_code
|
|
317
|
+
return rendered
|
|
268
318
|
except FileNotFoundError:
|
|
269
319
|
pass
|
|
270
320
|
except Exception as e:
|
|
@@ -275,7 +325,10 @@ class Response:
|
|
|
275
325
|
if fw_engine is not None:
|
|
276
326
|
try:
|
|
277
327
|
html = fw_engine.render(template, data or {})
|
|
278
|
-
|
|
328
|
+
rendered = self.html(html)
|
|
329
|
+
if status_code is not None:
|
|
330
|
+
rendered.status_code = status_code
|
|
331
|
+
return rendered
|
|
279
332
|
except FileNotFoundError:
|
|
280
333
|
pass
|
|
281
334
|
except Exception as e:
|