tina4-python 3.13.36__tar.gz → 3.13.38__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.36 → tina4_python-3.13.38}/.gitignore +2 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/PKG-INFO +1 -1
- {tina4_python-3.13.36 → tina4_python-3.13.38}/pyproject.toml +1 -1
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/CLAUDE.md +5 -3
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/HtmlElement.py +24 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/auth/__init__.py +131 -9
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/cli/__init__.py +91 -2
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/events.py +51 -9
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/middleware.py +33 -9
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/server.py +143 -35
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/connection.py +315 -52
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/firebird.py +6 -2
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/mssql.py +14 -3
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/mysql.py +15 -4
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/sqlite.py +7 -2
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/dev_admin/__init__.py +80 -62
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/dev_admin/metrics.py +172 -48
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/graphql/__init__.py +32 -9
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/mcp/tools.py +6 -2
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/messenger/__init__.py +117 -30
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/orm/model.py +10 -8
- tina4_python-3.13.38/tina4_python/public/js/tina4-dev-admin.js +1091 -0
- tina4_python-3.13.38/tina4_python/public/js/tina4-dev-admin.min.js +1091 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue_backends/kafka_backend.py +35 -1
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/seeder/__init__.py +354 -44
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/session/__init__.py +84 -9
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/websocket/__init__.py +284 -20
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/wsdl/__init__.py +15 -1
- tina4_python-3.13.36/tina4_python/public/js/tina4-dev-admin.js +0 -1091
- tina4_python-3.13.36/tina4_python/public/js/tina4-dev-admin.min.js +0 -1091
- {tina4_python-3.13.36 → tina4_python-3.13.38}/README.md +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/env.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.36 → tina4_python-3.13.38}/tina4_python/websocket/backplane.py +0 -0
|
@@ -1165,6 +1165,8 @@ queue.purge("completed")
|
|
|
1165
1165
|
|
|
1166
1166
|
Tina4 includes zero-config SOAP 1.1 support with automatic WSDL generation.
|
|
1167
1167
|
|
|
1168
|
+
**Security:** SOAP requests containing a `<!DOCTYPE>` (DTD) are rejected with a `Client` fault before parsing — SOAP 1.1 forbids DTDs, and this closes the XML entity-expansion (billion-laughs) and external-entity (XXE) attack surface. An operation that raises returns a `Server` fault whose `<faultstring>` is the real cause **only** in debug mode (`TINA4_DEBUG`); in production it is a generic "Internal server error" and the real cause is written to the log — so a resolver exception never leaks internal state to a SOAP client.
|
|
1169
|
+
|
|
1168
1170
|
```python
|
|
1169
1171
|
from typing import List, Optional
|
|
1170
1172
|
from tina4_python.wsdl import WSDL, wsdl_operation
|
|
@@ -1249,7 +1251,7 @@ result = gql.execute('{ users(limit: 3) { id name } }', variables={}, context={}
|
|
|
1249
1251
|
# {"data": {"users": [...]}}
|
|
1250
1252
|
```
|
|
1251
1253
|
|
|
1252
|
-
Supports: queries, mutations, variables, fragments, aliases, `@skip`/`@include` directives, nested selections, list types, inline fragments. Resolver exceptions are captured as GraphQL errors.
|
|
1254
|
+
Supports: queries, mutations, variables, fragments, aliases, `@skip`/`@include` directives, nested selections, list types, inline fragments. Resolver exceptions are captured as GraphQL errors — the message is the real cause only in debug mode (`TINA4_DEBUG`); in production it is a generic "Internal server error" (the real cause is logged) so a resolver exception never leaks internal state. **Depth guard:** selection-set nesting is bounded by `TINA4_GRAPHQL_MAX_DEPTH` (default `50`; set `<= 0` to disable). An over-deep query or a circular fragment fails with a `"Query exceeds maximum depth of N"` error instead of overflowing the stack.
|
|
1253
1255
|
|
|
1254
1256
|
| ORM Field | GraphQL Type |
|
|
1255
1257
|
|-----------|-------------|
|
|
@@ -1604,7 +1606,7 @@ Key `.env` settings:
|
|
|
1604
1606
|
|
|
1605
1607
|
```bash
|
|
1606
1608
|
# Authentication
|
|
1607
|
-
TINA4_SECRET=your-jwt-secret # JWT signing (
|
|
1609
|
+
TINA4_SECRET=your-jwt-secret # JWT signing. In DEV (TINA4_DEBUG truthy, not CI/prod) a blank value auto-generates a per-machine secret saved to gitignored .env.local; in CI/prod a blank value warns actionably (set it with `openssl rand -hex 32`)
|
|
1608
1610
|
TINA4_API_KEY=your-api-key # Static bearer token for API auth (API_KEY fallback supported)
|
|
1609
1611
|
TINA4_TOKEN_LIMIT=60 # Token lifetime in minutes (default: 60)
|
|
1610
1612
|
|
|
@@ -1853,7 +1855,7 @@ async def dashboard(request, response):
|
|
|
1853
1855
|
- **2,899 tests** passing across all modules
|
|
1854
1856
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1855
1857
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1856
|
-
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), DB query caching — request-scoped auto cache **
|
|
1858
|
+
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), DB query caching — request-scoped auto cache **off by default — opt-in via `TINA4_AUTO_CACHING=true`** (TTL `TINA4_AUTO_CACHING_TTL=5`s) dedupes identical reads within a request and flushes on writes; it ships OFF because a request-scoped cache can return pre-write state in a read-after-write (`SELECT MAX(id)` before an `INSERT` → duplicate keys), so opt in per read-heavy endpoint. Persistent cross-request cache also opt-in via `TINA4_DB_CACHE=true` (TTL `TINA4_DB_CACHE_TTL=30`s) routed through the unified backend set via `TINA4_DB_CACHE_BACKEND` (memory/file/redis/valkey/memcached/mongodb/database) + `TINA4_DB_CACHE_URL` so instances share one cache with global write-invalidation; `cache_stats()` reports `mode` (request/persistent/off) and `backend`, `cache_clear()`
|
|
1857
1859
|
- **Sessions**: 4 backends (file, Redis/Valkey, MongoDB, database)
|
|
1858
1860
|
- **Queue**: file/RabbitMQ/Kafka/MongoDB backends, configured via env vars
|
|
1859
1861
|
- **Cache**: unified backend set — memory (default), file, redis, valkey, memcached, mongodb, database — via `TINA4_CACHE_BACKEND` (+ `TINA4_CACHE_URL`/credentials); file-backend fallback if a backend is unreachable
|
|
@@ -17,6 +17,25 @@ Usage:
|
|
|
17
17
|
import html as _html
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class Raw(str):
|
|
21
|
+
"""Marker for trusted, pre-sanitised HTML that must render UNESCAPED.
|
|
22
|
+
|
|
23
|
+
String/scalar children of an HTMLElement are HTML-escaped by default to
|
|
24
|
+
prevent stored/reflected XSS. Wrap a value in Raw() to opt out of escaping
|
|
25
|
+
when (and only when) you have already sanitised it yourself.
|
|
26
|
+
|
|
27
|
+
HTMLElement("div")("<b>x</b>") # <b>x</b> (escaped)
|
|
28
|
+
HTMLElement("div")(Raw("<b>x</b>")) # <b>x</b> (raw)
|
|
29
|
+
|
|
30
|
+
Alias: SafeString.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Alias — some callers/frameworks prefer the SafeString name (matches Frond).
|
|
36
|
+
SafeString = Raw
|
|
37
|
+
|
|
38
|
+
|
|
20
39
|
class HTMLElement:
|
|
21
40
|
"""A single HTML element that renders itself and its children to a string."""
|
|
22
41
|
|
|
@@ -65,8 +84,13 @@ class HTMLElement:
|
|
|
65
84
|
|
|
66
85
|
for child in self.children:
|
|
67
86
|
if isinstance(child, HTMLElement):
|
|
87
|
+
# Nested elements render themselves (already escape their own children)
|
|
88
|
+
parts.append(str(child))
|
|
89
|
+
elif isinstance(child, Raw):
|
|
90
|
+
# Explicitly trusted markup — emit unescaped
|
|
68
91
|
parts.append(str(child))
|
|
69
92
|
else:
|
|
93
|
+
# Plain string/scalar child — escape to defeat XSS
|
|
70
94
|
parts.append(_html.escape(str(child), quote=True))
|
|
71
95
|
|
|
72
96
|
parts.append(f"</{self.tag}>")
|
|
@@ -49,24 +49,145 @@ class _DualMethod:
|
|
|
49
49
|
return functools.partial(self._func, obj)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _is_ci() -> bool:
|
|
53
|
+
"""True when running under a CI system.
|
|
54
|
+
|
|
55
|
+
Honours the de-facto ``CI`` env var that GitHub Actions, GitLab CI,
|
|
56
|
+
CircleCI, Travis, etc. all set to a truthy value. We never generate or
|
|
57
|
+
persist a dev secret in CI — a CI run with a blank secret must surface
|
|
58
|
+
the actionable warning, not silently mint one.
|
|
59
|
+
"""
|
|
60
|
+
from tina4_python.dotenv import is_truthy
|
|
61
|
+
return is_truthy(os.environ.get("CI"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_dev() -> bool:
|
|
65
|
+
"""True when the framework is in development mode (TINA4_DEBUG truthy)."""
|
|
66
|
+
from tina4_python.dotenv import is_truthy
|
|
67
|
+
return is_truthy(os.environ.get("TINA4_DEBUG"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_production() -> bool:
|
|
71
|
+
"""True when running in production (TINA4_ENV=production)."""
|
|
72
|
+
return os.environ.get("TINA4_ENV", "development") == "production"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Actionable message shown when TINA4_SECRET is blank in CI/prod — tells the
|
|
76
|
+
# operator exactly what to set and how. Kept as a constant so the bootstrap
|
|
77
|
+
# and the lazy resolver emit the identical guidance.
|
|
78
|
+
_BLANK_SECRET_WARNING = (
|
|
79
|
+
"Auth: TINA4_SECRET is not set — JWT signing is insecure. "
|
|
80
|
+
"Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) "
|
|
81
|
+
"in your environment or .env before serving traffic."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ensure_dev_secret(cwd: str = None) -> str | None:
|
|
86
|
+
"""Generate a per-machine development JWT secret once, at server boot.
|
|
87
|
+
|
|
88
|
+
Fail-safe default for local dev: a blank ``TINA4_SECRET`` used to log a
|
|
89
|
+
loud warning on every boot even though the developer was never told what
|
|
90
|
+
to set. Instead, in DEV (and only in dev), we mint a cryptographically
|
|
91
|
+
random secret, persist it to a gitignored ``.env.local`` so it survives
|
|
92
|
+
restarts, and set it in the process env for this run.
|
|
93
|
+
|
|
94
|
+
Generation happens ONLY when ALL of these hold:
|
|
95
|
+
* ``TINA4_SECRET`` is currently blank, AND
|
|
96
|
+
* we are in DEV (``TINA4_DEBUG`` truthy), AND
|
|
97
|
+
* we are NOT in CI (``CI`` env var not truthy), AND
|
|
98
|
+
* we are NOT in production (``TINA4_ENV`` != "production").
|
|
99
|
+
|
|
100
|
+
SECURITY: never generate or persist a secret in CI or production, and
|
|
101
|
+
only ever write to ``.env.local`` (gitignored) — never ``.env``. The
|
|
102
|
+
signing secret must never become a guessable built-in default.
|
|
103
|
+
|
|
104
|
+
In CI/prod with a blank secret, this emits the actionable warning instead
|
|
105
|
+
of generating anything.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cwd: Directory in which to create/append ``.env.local`` (defaults to
|
|
109
|
+
the current working directory). Used by tests to point at a temp
|
|
110
|
+
dir without chdir'ing the whole process.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The generated secret string when one was minted, else ``None``.
|
|
114
|
+
"""
|
|
115
|
+
if os.environ.get("TINA4_SECRET"):
|
|
116
|
+
return None # already configured — nothing to do
|
|
117
|
+
|
|
118
|
+
if not _is_dev() or _is_ci() or _is_production():
|
|
119
|
+
# CI / prod / non-dev with a blank secret: warn actionably, never mint.
|
|
120
|
+
_warn_blank_secret()
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Dev, not CI, not prod, blank secret → mint a per-machine dev secret.
|
|
124
|
+
new_secret = secrets.token_hex(32)
|
|
125
|
+
os.environ["TINA4_SECRET"] = new_secret # available for THIS run immediately
|
|
126
|
+
|
|
127
|
+
from pathlib import Path
|
|
128
|
+
base = Path(cwd) if cwd else Path.cwd()
|
|
129
|
+
env_local = base / ".env.local"
|
|
130
|
+
try:
|
|
131
|
+
# Append (create if missing). A trailing newline keeps the file
|
|
132
|
+
# parseable if it already held entries without a final newline.
|
|
133
|
+
prefix = ""
|
|
134
|
+
if env_local.exists():
|
|
135
|
+
existing = env_local.read_text(encoding="utf-8")
|
|
136
|
+
if existing and not existing.endswith("\n"):
|
|
137
|
+
prefix = "\n"
|
|
138
|
+
with env_local.open("a", encoding="utf-8") as fh:
|
|
139
|
+
fh.write(f"{prefix}TINA4_SECRET={new_secret}\n")
|
|
140
|
+
_log_info(
|
|
141
|
+
"Auth: generated a development secret, saved to .env.local (gitignored)"
|
|
142
|
+
)
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
# Never crash boot over a file write — keep the in-memory secret for
|
|
145
|
+
# this run and warn that it won't persist.
|
|
146
|
+
_log_warning(
|
|
147
|
+
"Auth: generated a development secret but could not write "
|
|
148
|
+
f".env.local ({exc}); using it for this run only"
|
|
149
|
+
)
|
|
150
|
+
return new_secret
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _log_info(message: str) -> None:
|
|
154
|
+
try:
|
|
155
|
+
from tina4_python.debug import Log
|
|
156
|
+
Log.info(message)
|
|
157
|
+
except Exception:
|
|
158
|
+
import sys
|
|
159
|
+
print(message, file=sys.stderr)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _log_warning(message: str) -> None:
|
|
163
|
+
try:
|
|
164
|
+
from tina4_python.debug import Log
|
|
165
|
+
Log.warning(message)
|
|
166
|
+
except Exception:
|
|
167
|
+
import sys
|
|
168
|
+
print(message, file=sys.stderr)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _warn_blank_secret() -> None:
|
|
172
|
+
_log_warning(_BLANK_SECRET_WARNING)
|
|
173
|
+
|
|
174
|
+
|
|
52
175
|
def _resolve_secret(secret: str = None) -> str:
|
|
53
176
|
"""Resolve the JWT signing secret.
|
|
54
177
|
|
|
55
178
|
Reads ``TINA4_SECRET`` from the environment. When neither an explicit
|
|
56
|
-
secret nor ``TINA4_SECRET`` is set, warns loudly
|
|
57
|
-
secret — parity with the PHP/Node frameworks.
|
|
58
|
-
with a guessable built-in default, which would
|
|
179
|
+
secret nor ``TINA4_SECRET`` is set, warns loudly with an actionable
|
|
180
|
+
message and returns a blank secret — parity with the PHP/Node frameworks.
|
|
181
|
+
Tina4 never silently signs with a guessable built-in default, which would
|
|
182
|
+
make tokens forgeable. (In dev, ``ensure_dev_secret()`` runs at boot and
|
|
183
|
+
mints + persists a secret so this blank path is hit only in CI/prod or
|
|
184
|
+
when auth is used before boot.)
|
|
59
185
|
"""
|
|
60
186
|
if secret:
|
|
61
187
|
return secret
|
|
62
188
|
env_secret = os.environ.get("TINA4_SECRET", "")
|
|
63
189
|
if not env_secret:
|
|
64
|
-
|
|
65
|
-
from tina4_python.debug import Log
|
|
66
|
-
Log.warning("Auth: TINA4_SECRET not set in .env — using blank secret (insecure)")
|
|
67
|
-
except Exception:
|
|
68
|
-
import sys
|
|
69
|
-
print("Auth: TINA4_SECRET not set in .env — using blank secret (insecure)", file=sys.stderr)
|
|
190
|
+
_warn_blank_secret()
|
|
70
191
|
return ""
|
|
71
192
|
return env_secret
|
|
72
193
|
|
|
@@ -403,4 +524,5 @@ class AuthMiddleware:
|
|
|
403
524
|
__all__ = [
|
|
404
525
|
"Auth", "AuthMiddleware", "get_token", "valid_token", "get_payload",
|
|
405
526
|
"refresh_token", "authenticate_request", "validate_api_key",
|
|
527
|
+
"ensure_dev_secret",
|
|
406
528
|
]
|
|
@@ -71,7 +71,7 @@ def _parse_fields(fields_str: str) -> list[tuple[str, str]]:
|
|
|
71
71
|
def _parse_flags(args: list[str]) -> tuple[dict, list[str]]:
|
|
72
72
|
"""Parse --key value and --flag from args. Returns (flags, positional)."""
|
|
73
73
|
# Boolean-only flags that never take a value argument
|
|
74
|
-
boolean_flags = {"no-browser", "no-reload", "production", "managed", "all", "clear"}
|
|
74
|
+
boolean_flags = {"no-browser", "no-reload", "production", "managed", "all", "clear", "json"}
|
|
75
75
|
|
|
76
76
|
flags = {}
|
|
77
77
|
positional = []
|
|
@@ -145,6 +145,7 @@ def main():
|
|
|
145
145
|
"ai": _ai,
|
|
146
146
|
"generate": _generate,
|
|
147
147
|
"console": _console,
|
|
148
|
+
"metrics": _metrics,
|
|
148
149
|
"help": _help,
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -222,6 +223,7 @@ Commands:
|
|
|
222
223
|
build Build distributable package
|
|
223
224
|
ai [--all] Install AI coding assistant context
|
|
224
225
|
console Start interactive REPL with framework loaded
|
|
226
|
+
metrics [--top N] [--json] [--fail-on warn|error] [--path DIR] Rank top code-quality offenders
|
|
225
227
|
|
|
226
228
|
Generators:
|
|
227
229
|
generate model <Name> [--fields "name:string,price:float"]
|
|
@@ -291,6 +293,93 @@ def _console(args=None):
|
|
|
291
293
|
code.interact(banner=banner, local=local_vars)
|
|
292
294
|
|
|
293
295
|
|
|
296
|
+
# ── Metrics ───────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def _metrics(args):
|
|
299
|
+
"""Report top code-quality offenders (complexity, size, maintainability, tests).
|
|
300
|
+
|
|
301
|
+
tina4python metrics # human report, scans src/ (or framework)
|
|
302
|
+
tina4python metrics --top 10 # only the worst 10
|
|
303
|
+
tina4python metrics --path tina4_python # scan a specific directory
|
|
304
|
+
tina4python metrics --json # machine-readable for CI
|
|
305
|
+
tina4python metrics --fail-on warn # exit 1 if any warn/error offender
|
|
306
|
+
tina4python metrics --fail-on error # exit 1 only on error-severity
|
|
307
|
+
"""
|
|
308
|
+
import json
|
|
309
|
+
from tina4_python.dev_admin import metrics as _m
|
|
310
|
+
|
|
311
|
+
flags, _ = _parse_flags(args)
|
|
312
|
+
|
|
313
|
+
top = int(flags["top"]) if "top" in flags and str(flags["top"]).isdigit() else 20
|
|
314
|
+
as_json = "json" in flags
|
|
315
|
+
path = flags.get("path", "src")
|
|
316
|
+
fail_on = flags.get("fail-on")
|
|
317
|
+
if fail_on not in (None, "warn", "error"):
|
|
318
|
+
print(f" invalid --fail-on '{fail_on}' (use warn or error)")
|
|
319
|
+
sys.exit(2)
|
|
320
|
+
|
|
321
|
+
result = _m.offenders(path, top=top)
|
|
322
|
+
summary = result["summary"]
|
|
323
|
+
found = result["offenders"]
|
|
324
|
+
|
|
325
|
+
if "error" in summary:
|
|
326
|
+
print(f" metrics error: {summary['error']}")
|
|
327
|
+
sys.exit(2)
|
|
328
|
+
|
|
329
|
+
# Decide exit code from the FULL offender set, not just the printed top-N.
|
|
330
|
+
# full_analysis is cached, so this reuses the same analysis.
|
|
331
|
+
all_offenders = _m.offenders(path, top=summary["total_offenders"] or 1)["offenders"]
|
|
332
|
+
severities = {o["severity"] for o in all_offenders}
|
|
333
|
+
exit_code = 0
|
|
334
|
+
if fail_on == "warn" and ({"warn", "error"} & severities):
|
|
335
|
+
exit_code = 1
|
|
336
|
+
elif fail_on == "error" and ("error" in severities):
|
|
337
|
+
exit_code = 1
|
|
338
|
+
|
|
339
|
+
if as_json:
|
|
340
|
+
print(json.dumps({"summary": summary, "offenders": found}, indent=2))
|
|
341
|
+
sys.exit(exit_code)
|
|
342
|
+
|
|
343
|
+
# ── Human report ──────────────────────────────────────────────────
|
|
344
|
+
use_color = sys.stdout.isatty()
|
|
345
|
+
|
|
346
|
+
def _c(text, code):
|
|
347
|
+
return f"\033[{code}m{text}\033[0m" if use_color else text
|
|
348
|
+
|
|
349
|
+
sev_color = {"error": "31", "warn": "33", "info": "2"} # red / yellow / dim
|
|
350
|
+
|
|
351
|
+
print()
|
|
352
|
+
print(f" Tina4 Metrics — {summary['scan_mode']} scan ({summary['scan_root']})")
|
|
353
|
+
print(f" files: {summary['files_analyzed']} "
|
|
354
|
+
f"functions: {summary['total_functions']} "
|
|
355
|
+
f"avg complexity: {summary['avg_complexity']} "
|
|
356
|
+
f"avg maintainability: {summary['avg_maintainability']}")
|
|
357
|
+
print(f" offenders: {summary['total_offenders']} total"
|
|
358
|
+
+ (f" (showing top {len(found)})" if found else ""))
|
|
359
|
+
print()
|
|
360
|
+
|
|
361
|
+
if not found:
|
|
362
|
+
print(" " + _c("✓ no offenders — clean", "32"))
|
|
363
|
+
print()
|
|
364
|
+
sys.exit(exit_code)
|
|
365
|
+
|
|
366
|
+
# Compute a column width for the file:line cell so the table lines up.
|
|
367
|
+
locs = [f"{o['file']}:{o['line']}" for o in found]
|
|
368
|
+
loc_w = max(len("FILE:LINE"), max(len(s) for s in locs))
|
|
369
|
+
kind_w = max(len("KIND"), max(len(o["kind"]) for o in found))
|
|
370
|
+
|
|
371
|
+
header = f" {'#':>3} {'SEVERITY':<8} {'KIND':<{kind_w}} {'FILE:LINE':<{loc_w}} DETAIL"
|
|
372
|
+
print(_c(header, "1"))
|
|
373
|
+
print(" " + "-" * (len(header) - 2))
|
|
374
|
+
for i, o in enumerate(found, 1):
|
|
375
|
+
sev = o["severity"]
|
|
376
|
+
sev_cell = _c(f"{sev:<8}", sev_color[sev])
|
|
377
|
+
print(f" {i:>3} {sev_cell} {o['kind']:<{kind_w}} "
|
|
378
|
+
f"{locs[i - 1]:<{loc_w}} {o['detail']}")
|
|
379
|
+
print()
|
|
380
|
+
sys.exit(exit_code)
|
|
381
|
+
|
|
382
|
+
|
|
294
383
|
# ── Init ──────────────────────────────────────────────────────────────
|
|
295
384
|
|
|
296
385
|
def _init(args):
|
|
@@ -363,7 +452,7 @@ def _init(args):
|
|
|
363
452
|
gitignore = target / ".gitignore"
|
|
364
453
|
if not gitignore.exists():
|
|
365
454
|
gitignore.write_text(
|
|
366
|
-
".env\n__pycache__/\n*.pyc\n.venv/\ndata/\nlogs/\n"
|
|
455
|
+
".env\n.env.local\n__pycache__/\n*.pyc\n.venv/\ndata/\nlogs/\n"
|
|
367
456
|
"sessions/\nsecrets/\n*.db\n",
|
|
368
457
|
encoding="utf-8",
|
|
369
458
|
)
|
|
@@ -68,30 +68,72 @@ def off(event: str, listener: callable = None):
|
|
|
68
68
|
]
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def _log_listener_error(event: str, error: Exception) -> None:
|
|
72
|
+
"""Log a listener failure without ever raising. Visible, never silent."""
|
|
73
|
+
try:
|
|
74
|
+
from tina4_python.debug import Log
|
|
75
|
+
Log.warning(
|
|
76
|
+
f"Event listener for '{event}' raised "
|
|
77
|
+
f"{type(error).__name__}: {error}"
|
|
78
|
+
)
|
|
79
|
+
except Exception:
|
|
80
|
+
# The logger itself must never break the event bus. Fall back to
|
|
81
|
+
# stderr so the failure is still surfaced, never swallowed.
|
|
82
|
+
import sys
|
|
83
|
+
print(
|
|
84
|
+
f"[tina4.events] listener for '{event}' raised "
|
|
85
|
+
f"{type(error).__name__}: {error}",
|
|
86
|
+
file=sys.stderr,
|
|
87
|
+
flush=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def emit(event: str, *args, strict: bool = False, **kwargs) -> list:
|
|
72
92
|
"""Fire an event synchronously. Returns list of listener results.
|
|
73
93
|
|
|
74
94
|
results = emit("user.created", user_data)
|
|
95
|
+
|
|
96
|
+
Each listener is isolated: if one raises, the error is LOGGED
|
|
97
|
+
(``Log.warning`` with the event name + error) and the remaining
|
|
98
|
+
listeners still run. A failed listener contributes a ``None`` slot to
|
|
99
|
+
the results list, so N listeners always yield N results in priority
|
|
100
|
+
order. Pass ``strict=True`` to RE-RAISE on the first listener error
|
|
101
|
+
instead of isolating it. Errors are never silently swallowed.
|
|
75
102
|
"""
|
|
76
103
|
results = []
|
|
77
104
|
for _, listener in _listeners.get(event, []):
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
try:
|
|
106
|
+
results.append(listener(*args, **kwargs))
|
|
107
|
+
except Exception as error:
|
|
108
|
+
if strict:
|
|
109
|
+
raise
|
|
110
|
+
_log_listener_error(event, error)
|
|
111
|
+
results.append(None)
|
|
80
112
|
return results
|
|
81
113
|
|
|
82
114
|
|
|
83
|
-
async def emit_async(event: str, *args, **kwargs) -> list:
|
|
115
|
+
async def emit_async(event: str, *args, strict: bool = False, **kwargs) -> list:
|
|
84
116
|
"""Fire an event, awaiting async listeners. Returns list of results.
|
|
85
117
|
|
|
86
118
|
results = await emit_async("order.placed", order)
|
|
119
|
+
|
|
120
|
+
Mirrors :func:`emit` — each awaited listener is isolated: one
|
|
121
|
+
rejection does not abort the others. On a listener error the failure
|
|
122
|
+
is LOGGED and a ``None`` slot is appended; ``strict=True`` re-raises
|
|
123
|
+
on the first error.
|
|
87
124
|
"""
|
|
88
125
|
results = []
|
|
89
126
|
for _, listener in _listeners.get(event, []):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
try:
|
|
128
|
+
if asyncio.iscoroutinefunction(listener):
|
|
129
|
+
results.append(await listener(*args, **kwargs))
|
|
130
|
+
else:
|
|
131
|
+
results.append(listener(*args, **kwargs))
|
|
132
|
+
except Exception as error:
|
|
133
|
+
if strict:
|
|
134
|
+
raise
|
|
135
|
+
_log_listener_error(event, error)
|
|
136
|
+
results.append(None)
|
|
95
137
|
return results
|
|
96
138
|
|
|
97
139
|
|
|
@@ -30,8 +30,18 @@ class Middleware:
|
|
|
30
30
|
"""Standardized middleware orchestrator.
|
|
31
31
|
|
|
32
32
|
Registers middleware classes globally and runs their ``before_*`` /
|
|
33
|
-
``after_*`` static methods
|
|
34
|
-
|
|
33
|
+
``after_*`` static methods. Mirrors the PHP, Ruby and Node.js
|
|
34
|
+
orchestrators.
|
|
35
|
+
|
|
36
|
+
Ordering rule (v3.13.38) — deterministic, never alphabetical:
|
|
37
|
+
|
|
38
|
+
* Across middleware classes: REGISTRATION order — the order they were
|
|
39
|
+
attached via ``Middleware.use`` / ``@middleware`` / ``Router.group``.
|
|
40
|
+
* Within a single class: DEFINITION order — ``before_*`` / ``after_*``
|
|
41
|
+
methods run in the order they appear in the class body (Python
|
|
42
|
+
preserves this in ``__dict__``), NOT ``dir()`` alphabetical order.
|
|
43
|
+
|
|
44
|
+
``before_*`` always runs before the route handler, ``after_*`` after.
|
|
35
45
|
"""
|
|
36
46
|
|
|
37
47
|
_global_middleware: list = []
|
|
@@ -81,13 +91,27 @@ class Middleware:
|
|
|
81
91
|
|
|
82
92
|
@staticmethod
|
|
83
93
|
def _discover_methods(mw_class, prefix: str) -> list:
|
|
84
|
-
"""Return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
"""Return prefixed method names in DEFINITION order (not alphabetical).
|
|
95
|
+
|
|
96
|
+
Walks the MRO from the base classes up to the most-derived so a
|
|
97
|
+
subclass's own methods run after the methods it inherits, and uses
|
|
98
|
+
each class's ``__dict__`` (insertion-ordered in Python 3.7+) to
|
|
99
|
+
preserve the order methods were written in the source. A subclass
|
|
100
|
+
override keeps the position of its first definition.
|
|
101
|
+
"""
|
|
102
|
+
seen = set()
|
|
103
|
+
names = []
|
|
104
|
+
klass = mw_class if isinstance(mw_class, type) else type(mw_class)
|
|
105
|
+
for base in reversed(klass.__mro__):
|
|
106
|
+
for name in base.__dict__:
|
|
107
|
+
if (
|
|
108
|
+
name.startswith(prefix)
|
|
109
|
+
and name not in seen
|
|
110
|
+
and callable(getattr(mw_class, name, None))
|
|
111
|
+
):
|
|
112
|
+
seen.add(name)
|
|
113
|
+
names.append(name)
|
|
114
|
+
return names
|
|
91
115
|
|
|
92
116
|
|
|
93
117
|
class CorsMiddleware:
|