tina4-python 3.13.39__tar.gz → 3.13.41__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.13.39 → tina4_python-3.13.41}/PKG-INFO +1 -1
- {tina4_python-3.13.39 → tina4_python-3.13.41}/pyproject.toml +1 -1
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/CLAUDE.md +31 -9
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/auth/__init__.py +5 -1
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/request.py +6 -1
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/server.py +29 -7
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/crud/__init__.py +5 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/__init__.py +33 -4
- tina4_python-3.13.41/tina4_python/docstore/__init__.py +714 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/__init__.py +63 -24
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/tools.py +18 -3
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/__init__.py +38 -8
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/lite_backend.py +132 -21
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/mongo_backend.py +8 -2
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/mongo_backend.py +71 -3
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/mongodb_handler.py +8 -3
- tina4_python-3.13.41/tina4_python/swagger/__init__.py +612 -0
- tina4_python-3.13.39/tina4_python/swagger/__init__.py +0 -388
- {tina4_python-3.13.39 → tina4_python-3.13.41}/.gitignore +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/README.md +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/env.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1145,9 +1145,14 @@ for job in queue.consume():
|
|
|
1145
1145
|
```python
|
|
1146
1146
|
queue = Queue(topic="tasks", max_retries=3)
|
|
1147
1147
|
|
|
1148
|
+
# Reservation / visibility timeout (seconds): a popped job is reserved this long;
|
|
1149
|
+
# if the consumer dies before complete()/fail() the next pop() reclaims it
|
|
1150
|
+
# (at-least-once delivery). Default 300; env TINA4_QUEUE_VISIBILITY_TIMEOUT; <= 0 disables.
|
|
1151
|
+
queue = Queue(topic="tasks", visibility_timeout=300)
|
|
1152
|
+
|
|
1148
1153
|
# Check queue size
|
|
1149
1154
|
queue.size() # pending jobs
|
|
1150
|
-
queue.size("reserved") # currently
|
|
1155
|
+
queue.size("reserved") # currently reserved (in-flight) jobs
|
|
1151
1156
|
|
|
1152
1157
|
# Retry failed jobs (under max_retries limit)
|
|
1153
1158
|
queue.retry_failed()
|
|
@@ -1159,6 +1164,14 @@ dead = queue.dead_letters()
|
|
|
1159
1164
|
queue.purge("completed")
|
|
1160
1165
|
```
|
|
1161
1166
|
|
|
1167
|
+
**At-least-once delivery (file + MongoDB backends).** A popped job is held as a
|
|
1168
|
+
reservation for `visibility_timeout` seconds. If the consumer crashes, OOMs, or
|
|
1169
|
+
is evicted before calling `job.complete()` / `job.fail()`, the next `pop()`
|
|
1170
|
+
reclaims the abandoned reservation: it increments `attempts` and re-enqueues the
|
|
1171
|
+
job, or dead-letters it once it has hit `max_retries`. A dead consumer therefore
|
|
1172
|
+
never strands a job. RabbitMQ and Kafka delegate redelivery to the broker (the
|
|
1173
|
+
framework timeout does not apply there).
|
|
1174
|
+
|
|
1162
1175
|
### When to use queues
|
|
1163
1176
|
- Sending emails or SMS
|
|
1164
1177
|
- Generating PDFs/reports
|
|
@@ -1633,14 +1646,23 @@ TINA4_SESSION_BACKEND=SessionFileHandler # SessionFileHandler, SessionRedisHand
|
|
|
1633
1646
|
TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
|
|
1634
1647
|
|
|
1635
1648
|
# Swagger/OpenAPI
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1649
|
+
TINA4_SWAGGER_ENABLED= # on/off for /swagger UI + /swagger/openapi.json. Explicit true/false wins; unset falls back to TINA4_DEBUG. Set false to DISABLE swagger in any environment; true to expose it in production
|
|
1650
|
+
TINA4_SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
|
|
1651
|
+
TINA4_SWAGGER_VERSION=1.0.0 # API version
|
|
1652
|
+
TINA4_SWAGGER_DESCRIPTION= # API description
|
|
1653
|
+
TINA4_SWAGGER_CONTACT_TEAM= # Contact name (legacy SWAGGER_CONTACT_TEAM still read as fallback)
|
|
1654
|
+
TINA4_SWAGGER_CONTACT_URL= # Contact URL (legacy SWAGGER_CONTACT_URL still read as fallback)
|
|
1655
|
+
TINA4_SWAGGER_CONTACT_EMAIL= # Contact email
|
|
1656
|
+
TINA4_SWAGGER_LICENSE= # SPDX license name (e.g. MIT) for info.license
|
|
1657
|
+
TINA4_SWAGGER_SERVERS= # comma-separated server URLs for the OpenAPI servers[] block; falls back to SWAGGER_DEV_URL
|
|
1658
|
+
TINA4_SWAGGER_UI_CDN= # base URL for the Swagger UI assets (default jsdelivr); point at a self-hosted mirror for air-gapped use
|
|
1659
|
+
SWAGGER_DEV_URL=http://localhost:7145 # single dev-server URL (used when TINA4_SWAGGER_SERVERS is unset)
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
The spec is OpenAPI 3.0.3. ORM models registered via AutoCrud become reusable
|
|
1663
|
+
`components.schemas` referenced by `$ref`, and secured routes emit a `bearerAuth`
|
|
1664
|
+
security requirement. Set `TINA4_SWAGGER_ENABLED=false` to turn the endpoints off
|
|
1665
|
+
anywhere (the production on/off switch, wired for real in 3.13.40).
|
|
1644
1666
|
|
|
1645
1667
|
### Debug levels
|
|
1646
1668
|
- `ALL` / `DEBUG` — most verbose; every level on the console
|
|
@@ -78,7 +78,11 @@ def _is_production() -> bool:
|
|
|
78
78
|
_BLANK_SECRET_WARNING = (
|
|
79
79
|
"Auth: TINA4_SECRET is not set — JWT signing is insecure. "
|
|
80
80
|
"Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) "
|
|
81
|
-
"in your environment or .env before serving traffic."
|
|
81
|
+
"in your environment or .env before serving traffic. "
|
|
82
|
+
"For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated "
|
|
83
|
+
"automatically into .env.local (gitignored). Seeing this warning means the "
|
|
84
|
+
"run was NOT detected as dev — typically a container or CI without "
|
|
85
|
+
"TINA4_DEBUG set, or TINA4_ENV=production."
|
|
82
86
|
)
|
|
83
87
|
|
|
84
88
|
|
|
@@ -71,7 +71,7 @@ class Request:
|
|
|
71
71
|
|
|
72
72
|
__slots__ = (
|
|
73
73
|
"method", "path", "url", "query_string", "params", "query", "headers",
|
|
74
|
-
"body", "raw_body", "cookies", "files", "ip",
|
|
74
|
+
"body", "raw_body", "cookies", "files", "ip", "remote_ip",
|
|
75
75
|
"content_type", "session", "_route_params",
|
|
76
76
|
)
|
|
77
77
|
|
|
@@ -88,6 +88,7 @@ class Request:
|
|
|
88
88
|
self.cookies: dict = {}
|
|
89
89
|
self.files: dict = {}
|
|
90
90
|
self.ip: str = ""
|
|
91
|
+
self.remote_ip: str = "" # Raw socket peer (never X-Forwarded-For) — for trust decisions
|
|
91
92
|
self.content_type: str = ""
|
|
92
93
|
self.session = None # Set by session middleware
|
|
93
94
|
self._route_params: dict = {} # Dynamic route params ({id}, etc.)
|
|
@@ -107,6 +108,10 @@ class Request:
|
|
|
107
108
|
|
|
108
109
|
req.content_type = req.headers.get("content-type", "")
|
|
109
110
|
req.ip = _extract_ip(scope, req.headers)
|
|
111
|
+
# Raw socket peer address — NEVER honours X-Forwarded-For, so it can
|
|
112
|
+
# be trusted for loopback/remote authorisation (e.g. the MCP guard).
|
|
113
|
+
_client = scope.get("client")
|
|
114
|
+
req.remote_ip = _client[0] if _client else ""
|
|
110
115
|
|
|
111
116
|
# Reconstruct the full absolute URL — scheme://host[:port]/path[?query].
|
|
112
117
|
# Honours x-forwarded-proto and x-forwarded-host so apps behind a proxy
|
|
@@ -1174,14 +1174,28 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
|
|
|
1174
1174
|
|
|
1175
1175
|
|
|
1176
1176
|
def _handle_swagger(request: Request, response: Response) -> Response | None:
|
|
1177
|
-
"""Serve /swagger UI and /swagger/openapi.json. Returns Response or None.
|
|
1177
|
+
"""Serve /swagger UI and /swagger/openapi.json. Returns Response or None.
|
|
1178
|
+
|
|
1179
|
+
Self-gated on swagger.is_enabled() (TINA4_SWAGGER_ENABLED, else TINA4_DEBUG)
|
|
1180
|
+
so the documented production on/off switch is actually honoured — before
|
|
1181
|
+
v3.13.40 the dispatch gated only on TINA4_DEBUG and this env var was dead.
|
|
1182
|
+
"""
|
|
1183
|
+
from tina4_python.swagger import is_enabled as _swagger_enabled
|
|
1184
|
+
if not _swagger_enabled():
|
|
1185
|
+
return None
|
|
1178
1186
|
if request.path in ("/swagger", "/swagger/"):
|
|
1187
|
+
# The UI assets load from a CDN by default (keeps the framework
|
|
1188
|
+
# zero-dependency — no vendored ~1.4MB swagger-ui-dist). Air-gapped
|
|
1189
|
+
# deployments point TINA4_SWAGGER_UI_CDN at a self-hosted mirror.
|
|
1190
|
+
_cdn = os.environ.get(
|
|
1191
|
+
"TINA4_SWAGGER_UI_CDN", "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5"
|
|
1192
|
+
).rstrip("/")
|
|
1179
1193
|
swagger_html = (
|
|
1180
1194
|
'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
|
|
1181
1195
|
'<title>API Documentation</title>'
|
|
1182
|
-
'<link rel="stylesheet" href="
|
|
1196
|
+
f'<link rel="stylesheet" href="{_cdn}/swagger-ui.css">'
|
|
1183
1197
|
'</head><body><div id="swagger-ui"></div>'
|
|
1184
|
-
'<script src="
|
|
1198
|
+
f'<script src="{_cdn}/swagger-ui-bundle.js"></script>'
|
|
1185
1199
|
'<script>SwaggerUIBundle({ url: "/swagger/openapi.json", dom_id: "#swagger-ui" });</script>'
|
|
1186
1200
|
'</body></html>'
|
|
1187
1201
|
)
|
|
@@ -1715,8 +1729,11 @@ async def handle(request: Request) -> Response:
|
|
|
1715
1729
|
):
|
|
1716
1730
|
return await _handle_dev_admin(request, response)
|
|
1717
1731
|
|
|
1718
|
-
# Swagger
|
|
1719
|
-
|
|
1732
|
+
# Swagger — _handle_swagger self-gates on swagger.is_enabled()
|
|
1733
|
+
# (TINA4_SWAGGER_ENABLED, else TINA4_DEBUG), so we call it on any GET:
|
|
1734
|
+
# this honours an explicit prod-enable AND an explicit dev-disable, both
|
|
1735
|
+
# of which the old `_is_dev`-only gate silently ignored.
|
|
1736
|
+
if request.method == "GET":
|
|
1720
1737
|
swagger_resp = _handle_swagger(request, response)
|
|
1721
1738
|
if swagger_resp is not None:
|
|
1722
1739
|
return swagger_resp
|
|
@@ -2227,8 +2244,13 @@ def _check_legacy_env_vars() -> None:
|
|
|
2227
2244
|
new = _LEGACY_ENV_VARS[old]
|
|
2228
2245
|
msg.append(f" {old:<28} → {new}")
|
|
2229
2246
|
msg.extend(["",
|
|
2230
|
-
"
|
|
2231
|
-
"
|
|
2247
|
+
"Note: these may come from a .env file loaded by dotenv, not just",
|
|
2248
|
+
"the runtime environment — check your image / build context (a .env",
|
|
2249
|
+
"baked into a Docker image is loaded at startup) as well as k8s/CI env.",
|
|
2250
|
+
"",
|
|
2251
|
+
"FIX: run `tina4 env --migrate` to rewrite your .env automatically",
|
|
2252
|
+
"(it renames every legacy name to its TINA4_ form in place).",
|
|
2253
|
+
"Or rename manually. See https://tina4.com/release/3.12.0",
|
|
2232
2254
|
"Set TINA4_ALLOW_LEGACY_ENV=true to bypass during migration.",
|
|
2233
2255
|
"─" * 72, ""])
|
|
2234
2256
|
print("\n".join(msg), file=sys.stderr)
|
|
@@ -132,6 +132,8 @@ class AutoCrud:
|
|
|
132
132
|
list_handler.__qualname__ = f"autocrud_list_{table}"
|
|
133
133
|
list_handler._swagger_summary = f"List all {pretty_name}"
|
|
134
134
|
list_handler._swagger_tags = [table]
|
|
135
|
+
list_handler._swagger_model = model_class
|
|
136
|
+
list_handler._swagger_model_list = True
|
|
135
137
|
Router.add("GET", base_path, list_handler)
|
|
136
138
|
generated.append({"method": "GET", "path": base_path, "table": table})
|
|
137
139
|
|
|
@@ -147,6 +149,7 @@ class AutoCrud:
|
|
|
147
149
|
get_handler.__qualname__ = f"autocrud_get_{table}"
|
|
148
150
|
get_handler._swagger_summary = f"Get {pretty_name} by ID"
|
|
149
151
|
get_handler._swagger_tags = [table]
|
|
152
|
+
get_handler._swagger_model = model_class
|
|
150
153
|
Router.add("GET", f"{base_path}/{{id}}", get_handler)
|
|
151
154
|
generated.append({"method": "GET", "path": f"{base_path}/{{id}}", "table": table})
|
|
152
155
|
|
|
@@ -169,6 +172,7 @@ class AutoCrud:
|
|
|
169
172
|
create_handler._swagger_summary = f"Create {pretty_name}"
|
|
170
173
|
create_handler._swagger_tags = [table]
|
|
171
174
|
create_handler._swagger_example = example_body
|
|
175
|
+
create_handler._swagger_model = model_class
|
|
172
176
|
Router.add("POST", base_path, create_handler)
|
|
173
177
|
generated.append({"method": "POST", "path": base_path, "table": table})
|
|
174
178
|
|
|
@@ -201,6 +205,7 @@ class AutoCrud:
|
|
|
201
205
|
update_handler._swagger_summary = f"Update {pretty_name}"
|
|
202
206
|
update_handler._swagger_tags = [table]
|
|
203
207
|
update_handler._swagger_example = example_body
|
|
208
|
+
update_handler._swagger_model = model_class
|
|
204
209
|
Router.add("PUT", f"{base_path}/{{id}}", update_handler)
|
|
205
210
|
generated.append({"method": "PUT", "path": f"{base_path}/{{id}}", "table": table})
|
|
206
211
|
|
|
@@ -2864,12 +2864,42 @@ async def _api_git_status(request, response):
|
|
|
2864
2864
|
# the same registry, so tools registered via the `@mcp_tool` decorator
|
|
2865
2865
|
# show up on both immediately.
|
|
2866
2866
|
|
|
2867
|
+
def _mcp_token_ok(request) -> bool:
|
|
2868
|
+
"""Timing-safe check of a remote MCP caller's token against
|
|
2869
|
+
TINA4_MCP_TOKEN (falling back to TINA4_API_KEY). Accepts an
|
|
2870
|
+
`Authorization: Bearer <token>` header, `X-MCP-Token`, or `X-Api-Key`."""
|
|
2871
|
+
import os as _os, hmac as _hmac
|
|
2872
|
+
expected = _os.environ.get("TINA4_MCP_TOKEN") or _os.environ.get("TINA4_API_KEY") or ""
|
|
2873
|
+
if not expected:
|
|
2874
|
+
return False
|
|
2875
|
+
headers = getattr(request, "headers", None) or {}
|
|
2876
|
+
auth = headers.get("authorization", "") or ""
|
|
2877
|
+
provided = auth[7:].strip() if auth[:7].lower() == "bearer " else ""
|
|
2878
|
+
if not provided:
|
|
2879
|
+
provided = headers.get("x-mcp-token", "") or headers.get("x-api-key", "")
|
|
2880
|
+
if not provided:
|
|
2881
|
+
return False
|
|
2882
|
+
return _hmac.compare_digest(str(provided), str(expected))
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
def _mcp_request_allowed(request) -> bool:
|
|
2886
|
+
"""Per-request MCP authorisation gate applied by EVERY MCP surface
|
|
2887
|
+
(JSON-RPC, SSE, and the REST shim). Loopback callers are allowed; a
|
|
2888
|
+
non-loopback (remote) caller needs TINA4_MCP_REMOTE + a valid token.
|
|
2889
|
+
Uses the raw socket peer (request.remote_ip), never X-Forwarded-For."""
|
|
2890
|
+
from tina4_python.mcp import is_request_allowed
|
|
2891
|
+
remote_ip = getattr(request, "remote_ip", "") or ""
|
|
2892
|
+
return is_request_allowed(remote_ip, _mcp_token_ok(request))
|
|
2893
|
+
|
|
2894
|
+
|
|
2867
2895
|
async def _api_mcp_tools(request, response):
|
|
2868
2896
|
"""GET — return the MCP tool registry as a plain JSON list.
|
|
2869
2897
|
|
|
2870
2898
|
Shape matches what dev-admin's `listMcpTools()` expects:
|
|
2871
2899
|
{"tools": [{"name": "...", "description": "...", "schema": {...}}, ...]}
|
|
2872
2900
|
"""
|
|
2901
|
+
if not _mcp_request_allowed(request):
|
|
2902
|
+
return response({"tools": [], "error": "MCP forbidden"}, 404)
|
|
2873
2903
|
try:
|
|
2874
2904
|
from tina4_python.mcp import _get_default_server
|
|
2875
2905
|
server = _get_default_server()
|
|
@@ -2950,8 +2980,8 @@ async def _api_mcp_rpc(request, response):
|
|
|
2950
2980
|
(no id) yield an empty 204.
|
|
2951
2981
|
"""
|
|
2952
2982
|
import json as _json
|
|
2953
|
-
from tina4_python.mcp import _get_default_server
|
|
2954
|
-
if not
|
|
2983
|
+
from tina4_python.mcp import _get_default_server
|
|
2984
|
+
if not _mcp_request_allowed(request):
|
|
2955
2985
|
return response({"error": "MCP disabled"}, 404)
|
|
2956
2986
|
server = _get_default_server()
|
|
2957
2987
|
body = request.body
|
|
@@ -2966,8 +2996,7 @@ async def _api_mcp_sse(request, response):
|
|
|
2966
2996
|
"""GET — SSE handshake. Emits the `endpoint` event telling the client
|
|
2967
2997
|
where to POST JSON-RPC messages, per the MCP HTTP+SSE transport.
|
|
2968
2998
|
"""
|
|
2969
|
-
|
|
2970
|
-
if not is_enabled():
|
|
2999
|
+
if not _mcp_request_allowed(request):
|
|
2971
3000
|
return response({"error": "MCP disabled"}, 404)
|
|
2972
3001
|
base = request.path.rsplit("/sse", 1)[0]
|
|
2973
3002
|
sse = f"event: endpoint\ndata: {base}/message\n\n"
|