tina4-python 3.13.97__tar.gz → 3.13.99__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.97 → tina4_python-3.13.99}/PKG-INFO +2 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/pyproject.toml +1 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/CLAUDE.md +17 -19
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/Testing.py +21 -12
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/__init__.py +7 -3
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/api/__init__.py +14 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/auth/__init__.py +5 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/cli/__init__.py +100 -85
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/middleware.py +157 -29
- tina4_python-3.13.99/tina4_python/core/port_takeover.py +309 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/request.py +200 -15
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/response.py +7 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/router.py +23 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/server.py +394 -165
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/crud/__init__.py +100 -17
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/adapter.py +192 -68
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/connection.py +71 -22
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/firebird.py +72 -15
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mongodb.py +71 -15
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mssql.py +61 -11
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mysql.py +94 -18
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/odbc.py +72 -14
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/postgres.py +2 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/sql_translator.py +130 -19
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/sqlite.py +55 -9
- tina4_python-3.13.99/tina4_python/debug/__init__.py +902 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/debug/error_overlay.py +60 -34
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/__init__.py +141 -25
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/engine.py +23 -3
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +1 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/__init__.py +10 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/messenger/__init__.py +25 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/migration/runner.py +26 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/__init__.py +2 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/fields.py +223 -54
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/model.py +266 -70
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4-dev-admin.min.js +23 -19
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/__init__.py +2 -2
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/seeder/__init__.py +46 -6
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/swagger/__init__.py +46 -8
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/test_client/__init__.py +21 -1
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/validator/__init__.py +7 -1
- tina4_python-3.13.97/tina4_python/debug/__init__.py +0 -689
- tina4_python-3.13.97/tina4_python/scss/tina4css/_alerts.scss +0 -34
- tina4_python-3.13.97/tina4_python/scss/tina4css/_badges.scss +0 -22
- tina4_python-3.13.97/tina4_python/scss/tina4css/_buttons.scss +0 -69
- tina4_python-3.13.97/tina4_python/scss/tina4css/_cards.scss +0 -49
- tina4_python-3.13.97/tina4_python/scss/tina4css/_forms.scss +0 -156
- tina4_python-3.13.97/tina4_python/scss/tina4css/_grid.scss +0 -81
- tina4_python-3.13.97/tina4_python/scss/tina4css/_modals.scss +0 -84
- tina4_python-3.13.97/tina4_python/scss/tina4css/_nav.scss +0 -149
- tina4_python-3.13.97/tina4_python/scss/tina4css/_pagination.scss +0 -63
- tina4_python-3.13.97/tina4_python/scss/tina4css/_reset.scss +0 -94
- tina4_python-3.13.97/tina4_python/scss/tina4css/_tables.scss +0 -54
- tina4_python-3.13.97/tina4_python/scss/tina4css/_typography.scss +0 -55
- tina4_python-3.13.97/tina4_python/scss/tina4css/_utilities.scss +0 -208
- tina4_python-3.13.97/tina4_python/scss/tina4css/_variables.scss +0 -117
- tina4_python-3.13.97/tina4_python/scss/tina4css/base.scss +0 -1
- tina4_python-3.13.97/tina4_python/scss/tina4css/colors.scss +0 -48
- tina4_python-3.13.97/tina4_python/scss/tina4css/tina4.scss +0 -18
- {tina4_python-3.13.97 → tina4_python-3.13.99}/.gitignore +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/README.md +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/database_url.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/env.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/amqp_url.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/memcached_handler.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -238,7 +238,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
|
|
|
238
238
|
| Static file serving | Put files in `src/public/` — auto-served |
|
|
239
239
|
| Translations / i18n | `I18n` from `tina4_python.i18n` |
|
|
240
240
|
| HTML generation in code | `HTMLElement` from `tina4_python.HtmlElement` |
|
|
241
|
-
| Inline testing | `@tests`, `
|
|
241
|
+
| Inline testing | `@tests`, `expect_equal`, `expect_raises` from `tina4_python.Testing` |
|
|
242
242
|
| Event system | `on`, `emit`, `once`, `off` from `tina4_python.core.events` |
|
|
243
243
|
| AI assistant context | `detect_ai`, `install_context` from `tina4_python.ai` |
|
|
244
244
|
| Response caching | `ResponseCache`, `cache_stats`, `clear_cache` from `tina4_python.cache` |
|
|
@@ -357,7 +357,7 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
357
357
|
├── dotenv/ # .env file loader
|
|
358
358
|
├── cli/ # CLI commands
|
|
359
359
|
├── HtmlElement.py # Programmatic HTML builder (HTMLElement, add_html_helpers)
|
|
360
|
-
├── Testing.py # Inline testing framework (tests,
|
|
360
|
+
├── Testing.py # Inline testing framework (tests, expect_equal, run_all)
|
|
361
361
|
├── templates/ # Built-in framework templates (Twig)
|
|
362
362
|
├── public/ # Built-in static assets
|
|
363
363
|
└── scss/ # Built-in SCSS
|
|
@@ -973,7 +973,7 @@ uv run tina4python migrate
|
|
|
973
973
|
- Files are executed in **numeric-prefix order** (`9_` before `10_`) and split on the `;` delimiter. A file without a numeric/timestamp prefix logs a warning — its order is undefined
|
|
974
974
|
- State is tracked in the `tina4_migration` table (auto-created per engine, canonical columns `id, migration_name VARCHAR(500) NOT NULL UNIQUE, description VARCHAR(500), batch INTEGER NOT NULL DEFAULT 1, executed_at VARCHAR(50) NOT NULL, passed INTEGER NOT NULL DEFAULT 1` — identical across all four frameworks). A migration is **applied** when a row exists for it with `passed = 1` (the applied-read is `WHERE passed = 1`). `migrate()` writes **only `passed = 1` rows**: on a failure the file's transaction is rolled back and **no row is written** for it (it is NOT recorded as `passed=0`), and `migrate()` raises. The public `record_migration(name, batch, passed=...)` API can write a `passed=0` row; any `passed=0` row (including one carried over from a v2 table) is treated as **not applied**, so it is reported pending. A leftover `passed=0` row (a prior failure or a v2 carry-over) **re-applies cleanly**: on the next `migrate()` the success path deletes any existing row for that `migration_name` before writing the fresh `passed=1` row, so the migration runs again instead of colliding on the unique `migration_name` — the bookkeeping table holds at most one row per migration with its latest state. The v2->v3 upgrade logs a warning naming any `passed=0` rows so you know which ones will re-run.
|
|
975
975
|
- **Each migration FILE is wrapped in its own transaction**: on a failure the file rolls back and `migrate()` **raises** (it does not write `passed=0`, delete anything, or `sys.exit`). Already-applied files stay applied — fix the bad file and re-run. The explicit `tina4 migrate` CLI surfaces the raise as a non-zero exit; startup auto-migration logs it and the service still boots (see TINA4_AUTO_MIGRATE above).
|
|
976
|
-
- **Atomicity caveat:** per-file transactions are truly atomic
|
|
976
|
+
- **Atomicity caveat:** per-file transactions are truly atomic on engines with **transactional DDL (PostgreSQL, and SQLite)**. SQLite's DDL is transactional too (autocommit is off inside `start_transaction()`), so a multi-statement migration that fails midway on SQLite rolls back cleanly, including any `CREATE TABLE` that already ran in the same file — proven by `tests/test_migration.py::TestMigratePositive::test_partial_failure_rolls_back`. MySQL and Firebird **auto-commit DDL**, so the same failure on those two engines leaves earlier statements applied — keep one logical change per file there. CREATE TABLE / ALTER-ADD are made idempotent on Firebird/MSSQL (existence-checked) so a re-run doesn't error.
|
|
977
977
|
|
|
978
978
|
### Engine-specific DDL patterns
|
|
979
979
|
|
|
@@ -1369,11 +1369,11 @@ Supports all HTML tags, void tags (`<br>`, `<img>`, `<input>`, etc.), and auto-e
|
|
|
1369
1369
|
Tina4 includes a decorator-based test framework for inline test cases:
|
|
1370
1370
|
|
|
1371
1371
|
```python
|
|
1372
|
-
from tina4_python.Testing import tests,
|
|
1372
|
+
from tina4_python.Testing import tests, expect_equal, expect_raises
|
|
1373
1373
|
|
|
1374
1374
|
@tests(
|
|
1375
|
-
|
|
1376
|
-
|
|
1375
|
+
expect_equal((5, 3), 8),
|
|
1376
|
+
expect_raises(ValueError, (None,))
|
|
1377
1377
|
)
|
|
1378
1378
|
def add(a, b=None):
|
|
1379
1379
|
if b is None:
|
|
@@ -1383,7 +1383,7 @@ def add(a, b=None):
|
|
|
1383
1383
|
|
|
1384
1384
|
Run all decorated tests:
|
|
1385
1385
|
```bash
|
|
1386
|
-
uv run tina4python test # Discovers @tests in src/**/*.py
|
|
1386
|
+
uv run tina4python test # Discovers @tests in src/**/*.py, real exit code
|
|
1387
1387
|
```
|
|
1388
1388
|
|
|
1389
1389
|
Or programmatically:
|
|
@@ -1542,16 +1542,14 @@ container.get("unknown") # KeyError: service not registered: unknown
|
|
|
1542
1542
|
Rich, syntax-highlighted HTML error overlay for development mode. Shows stack traces with source code context, request details, and environment info.
|
|
1543
1543
|
|
|
1544
1544
|
```python
|
|
1545
|
-
from tina4_python.debug.error_overlay import render_error_overlay,
|
|
1545
|
+
from tina4_python.debug.error_overlay import render_error_overlay, is_debug_mode
|
|
1546
1546
|
|
|
1547
|
-
#
|
|
1547
|
+
# Dev only — the caller gates the overlay on is_debug_mode()
|
|
1548
1548
|
if is_debug_mode(): # True when TINA4_DEBUG is true
|
|
1549
|
-
html = render_error_overlay(exception, request
|
|
1550
|
-
else:
|
|
1551
|
-
html = render_production_error(status_code=500, message="Internal Server Error")
|
|
1549
|
+
html = render_error_overlay(exception, request)
|
|
1552
1550
|
```
|
|
1553
1551
|
|
|
1554
|
-
The error overlay is
|
|
1552
|
+
The error overlay is dev-only (gated on `is_debug_mode()`/`TINA4_DEBUG`). The production 500 is NOT rendered here — the server dispatch renders `errors/500.twig` with an empty `error_message` (CWE-209), so the exception detail stays in the server log only. Sensitive request fields (Authorization / Cookie / Set-Cookie headers and password-like body/param keys) are redacted even in the overlay, the frame count is capped, and the caller guards the render.
|
|
1555
1553
|
|
|
1556
1554
|
## HtmlElement — Programmatic HTML Builder
|
|
1557
1555
|
|
|
@@ -1587,14 +1585,14 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
|
|
|
1587
1585
|
Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
|
|
1588
1586
|
|
|
1589
1587
|
```python
|
|
1590
|
-
from tina4_python.Testing import tests,
|
|
1588
|
+
from tina4_python.Testing import tests, expect_equal, expect_raises, expect_true, expect_false, run_all
|
|
1591
1589
|
|
|
1592
1590
|
@tests(
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1591
|
+
expect_equal((5, 3), 8), # add(5, 3) == 8
|
|
1592
|
+
expect_equal((0, 0), 0), # add(0, 0) == 0
|
|
1593
|
+
expect_raises(ValueError, (None,)), # add(None) raises ValueError
|
|
1594
|
+
expect_true((1, 1)), # add(1, 1) is truthy
|
|
1595
|
+
expect_false((0, 0)), # add(0, 0) is falsy
|
|
1598
1596
|
)
|
|
1599
1597
|
def add(a, b=None):
|
|
1600
1598
|
if b is None:
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
# License: MIT https://opensource.org/licenses/MIT
|
|
4
4
|
"""
|
|
5
5
|
Inline testing framework — decorate functions with test assertions
|
|
6
|
-
that run alongside the code
|
|
6
|
+
that run alongside the code, and run them with ``tina4 test``.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
from tina4_python.Testing import tests,
|
|
9
|
+
from tina4_python.Testing import tests, expect_equal, expect_raises
|
|
10
10
|
|
|
11
11
|
@tests(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
expect_equal((5, 3), 8),
|
|
13
|
+
expect_raises(ValueError, (None,))
|
|
14
14
|
)
|
|
15
15
|
def add(a, b=None):
|
|
16
16
|
if b is None:
|
|
@@ -20,6 +20,15 @@ Usage:
|
|
|
20
20
|
Run all tests:
|
|
21
21
|
from tina4_python.Testing import run_all
|
|
22
22
|
run_all()
|
|
23
|
+
|
|
24
|
+
The builders are named ``expect_*`` — deliberately distinct from the xUnit
|
|
25
|
+
``assert_*`` assertions in ``tina4_python.test`` (which take positional
|
|
26
|
+
``(actual, expected, message)`` and assert immediately). ``expect_*`` are
|
|
27
|
+
DESCRIPTORS: ``expect_equal((args,), expected)`` records "call the decorated
|
|
28
|
+
function with ``args`` and check the result equals ``expected``" for the
|
|
29
|
+
runner to execute later. Keeping the two surfaces' names apart means importing
|
|
30
|
+
the wrong one is a loud ``ImportError``/``AttributeError``, never a silent
|
|
31
|
+
change of call semantics.
|
|
23
32
|
"""
|
|
24
33
|
|
|
25
34
|
import sys
|
|
@@ -32,23 +41,23 @@ _registry: list[dict] = []
|
|
|
32
41
|
|
|
33
42
|
# ── Assertion builders ──────────────────────────────────────────────
|
|
34
43
|
|
|
35
|
-
def
|
|
36
|
-
"""
|
|
44
|
+
def expect_equal(args: tuple, expected):
|
|
45
|
+
"""Expect that calling the decorated function with *args* returns *expected*."""
|
|
37
46
|
return {"type": "equal", "args": args, "expected": expected}
|
|
38
47
|
|
|
39
48
|
|
|
40
|
-
def
|
|
41
|
-
"""
|
|
49
|
+
def expect_raises(exception_class: type, args: tuple):
|
|
50
|
+
"""Expect that calling the decorated function with *args* raises *exception_class*."""
|
|
42
51
|
return {"type": "raises", "exception": exception_class, "args": args}
|
|
43
52
|
|
|
44
53
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
54
|
+
def expect_true(args: tuple):
|
|
55
|
+
"""Expect that calling the decorated function with *args* returns a truthy value."""
|
|
47
56
|
return {"type": "true", "args": args}
|
|
48
57
|
|
|
49
58
|
|
|
50
|
-
def
|
|
51
|
-
"""
|
|
59
|
+
def expect_false(args: tuple):
|
|
60
|
+
"""Expect that calling the decorated function with *args* returns a falsy value."""
|
|
52
61
|
return {"type": "false", "args": args}
|
|
53
62
|
|
|
54
63
|
|
|
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
|
|
|
51
51
|
#
|
|
52
52
|
# test_version_constant.py now asserts this literal equals the pyproject
|
|
53
53
|
# version, so the release bump cannot leave it behind again.
|
|
54
|
-
return "3.13.
|
|
54
|
+
return "3.13.99"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
__version__ = _resolve_version()
|
|
@@ -106,6 +106,7 @@ _LAZY: dict[str, tuple[str, str]] = {
|
|
|
106
106
|
"TextField": ("tina4_python.orm", "TextField"),
|
|
107
107
|
"BlobField": ("tina4_python.orm", "BlobField"),
|
|
108
108
|
"NumericField": ("tina4_python.orm", "NumericField"),
|
|
109
|
+
"DecimalField": ("tina4_python.orm", "DecimalField"),
|
|
109
110
|
"JSONField": ("tina4_python.orm", "JSONField"),
|
|
110
111
|
"ForeignKeyField": ("tina4_python.orm", "ForeignKeyField"),
|
|
111
112
|
"IntField": ("tina4_python.orm", "IntField"),
|
|
@@ -148,9 +149,12 @@ _LAZY: dict[str, tuple[str, str]] = {
|
|
|
148
149
|
"StorageBackend": ("tina4_python.realtime.storage", "StorageBackend"),
|
|
149
150
|
"LocalStorage": ("tina4_python.realtime.storage", "LocalStorage"),
|
|
150
151
|
"S3Storage": ("tina4_python.realtime.storage", "S3Storage"),
|
|
151
|
-
# Inline testing
|
|
152
|
+
# Inline testing (decorator/descriptor surface — expect_* builders)
|
|
152
153
|
"tests": ("tina4_python.Testing", "tests"),
|
|
153
|
-
"
|
|
154
|
+
"expect_equal": ("tina4_python.Testing", "expect_equal"),
|
|
155
|
+
"expect_raises": ("tina4_python.Testing", "expect_raises"),
|
|
156
|
+
"expect_true": ("tina4_python.Testing", "expect_true"),
|
|
157
|
+
"expect_false": ("tina4_python.Testing", "expect_false"),
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
|
|
@@ -24,6 +24,12 @@ from urllib.parse import urlencode, urlparse
|
|
|
24
24
|
from urllib.request import Request, HTTPRedirectHandler, HTTPSHandler, build_opener
|
|
25
25
|
from urllib.error import HTTPError, URLError
|
|
26
26
|
|
|
27
|
+
# Safe: importing the tina4_python.api submodule always fully initializes the
|
|
28
|
+
# tina4_python package first, and __version__ is set near the very top of
|
|
29
|
+
# tina4_python/__init__.py, well before Api (a LAZY-loaded name) is ever
|
|
30
|
+
# touched -- no circular import.
|
|
31
|
+
from tina4_python import __version__
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
# Statuses that warrant an automatic retry when ``max_retries`` > 0: rate-limit
|
|
29
35
|
# (429) plus the transient server-side 5xx family. 4xx client errors (401,
|
|
@@ -382,7 +388,14 @@ class Api:
|
|
|
382
388
|
|
|
383
389
|
def _build_request(self, method: str, url: str, body, content_type: str,
|
|
384
390
|
extra_headers: dict | None = None) -> Request:
|
|
385
|
-
|
|
391
|
+
# VERSION-DEC-03 (feature 130): every outbound request carries a
|
|
392
|
+
# default ``Tina4/<version>`` User-Agent. ``self._headers`` is merged
|
|
393
|
+
# in AFTER the default, and ``extra_headers`` after that, so a
|
|
394
|
+
# caller-supplied User-Agent (via the constructor's ``headers=``,
|
|
395
|
+
# ``add_headers()``, or a per-call ``extra_headers``) always wins --
|
|
396
|
+
# this is a default, never a clobber.
|
|
397
|
+
headers = {"User-Agent": f"Tina4/{__version__}"}
|
|
398
|
+
headers.update(self._headers)
|
|
386
399
|
if self.auth_header:
|
|
387
400
|
headers["Authorization"] = self.auth_header
|
|
388
401
|
|
|
@@ -783,7 +783,11 @@ class AuthMiddleware:
|
|
|
783
783
|
token = auth_header[7:]
|
|
784
784
|
if not Auth.valid_token_static(token):
|
|
785
785
|
return request, response({"error": "Invalid token"}, 401)
|
|
786
|
-
request.
|
|
786
|
+
# Stash the payload on request.user (REQ-PY-NO-USER, 3.13.99 — the
|
|
787
|
+
# field every other framework already exposes). This used to assign
|
|
788
|
+
# request.auth, which __slots__ has never declared, so this branch
|
|
789
|
+
# raised AttributeError on every SUCCESSFUL authentication.
|
|
790
|
+
request.user = Auth.get_payload_static(token)
|
|
787
791
|
return request, response
|
|
788
792
|
|
|
789
793
|
|
|
@@ -128,8 +128,8 @@ def _fields_or_default(fields_str: str) -> list[tuple[str, str]]:
|
|
|
128
128
|
def _parse_flags(args: list[str]) -> tuple[dict, list[str]]:
|
|
129
129
|
"""Parse --key value and --flag from args. Returns (flags, positional)."""
|
|
130
130
|
# Boolean-only flags that never take a value argument
|
|
131
|
-
boolean_flags = {"no-browser", "no-reload", "production", "managed", "all", "clear",
|
|
132
|
-
"public", "no-migration", "once"}
|
|
131
|
+
boolean_flags = {"no-browser", "no-reload", "no-kill", "production", "managed", "all", "clear",
|
|
132
|
+
"json", "public", "no-migration", "once"}
|
|
133
133
|
|
|
134
134
|
flags = {}
|
|
135
135
|
positional = []
|
|
@@ -221,87 +221,37 @@ def _parse_every(every: str) -> int:
|
|
|
221
221
|
return 60
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
blob = fh.read()
|
|
237
|
-
return "docker" in blob or "containerd" in blob or "kubepods" in blob
|
|
238
|
-
except OSError:
|
|
239
|
-
return False
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def selectable_pids(lsof_output: str, me: int, my_group: int | None = None) -> list[int]:
|
|
243
|
-
"""The PIDs from ``lsof -ti`` output that are safe to signal.
|
|
244
|
-
|
|
245
|
-
Pure so the safety rule can be tested directly. An unvalidated parse is a
|
|
246
|
-
footgun with real teeth: when ``lsof`` is present but prints a different
|
|
247
|
-
shape than ``-ti`` implies, a non-numeric field coerces to 0, and
|
|
248
|
-
signalling PID 0 sends the signal to EVERY process in the caller's own
|
|
249
|
-
process group -- the server kills itself. That is exactly what happened in
|
|
250
|
-
a container, where the log read "Killed existing process on port 7148
|
|
251
|
-
(PID: 1 ...)" and the container exited 143.
|
|
252
|
-
|
|
253
|
-
So: accept only all-digit tokens, and never PID 0 (our process group),
|
|
254
|
-
PID 1 (init), ourselves, or our own process group.
|
|
255
|
-
"""
|
|
256
|
-
pids = []
|
|
257
|
-
for token in lsof_output.split():
|
|
258
|
-
if not token.isdigit():
|
|
259
|
-
continue # never coerce junk into a PID
|
|
260
|
-
pid = int(token)
|
|
261
|
-
if pid <= 1 or pid == me:
|
|
262
|
-
continue # 0 = our process group, 1 = init, me = suicide
|
|
263
|
-
if my_group is not None and pid == my_group:
|
|
264
|
-
continue
|
|
265
|
-
if pid not in pids:
|
|
266
|
-
pids.append(pid)
|
|
267
|
-
return pids
|
|
224
|
+
# The port-takeover safety logic (identity check, PID safety filter, container
|
|
225
|
+
# guard, dev gate, opt-out) lives in ONE shared module so the CLI path here and
|
|
226
|
+
# the runtime bind-failure fallback in core/server.py cannot diverge
|
|
227
|
+
# (TAKEOVER-DEC-02). `selectable_pids` and `_in_container` are re-exported so
|
|
228
|
+
# their existing callers/tests keep resolving `tina4_python.cli.selectable_pids`.
|
|
229
|
+
from tina4_python.core.port_takeover import ( # noqa: E402
|
|
230
|
+
selectable_pids,
|
|
231
|
+
in_container as _in_container,
|
|
232
|
+
take_over_port,
|
|
233
|
+
is_dev,
|
|
234
|
+
no_takeover_opted_out,
|
|
235
|
+
)
|
|
268
236
|
|
|
269
237
|
|
|
270
238
|
def _kill_process_on_port(port: int) -> bool:
|
|
271
|
-
"""
|
|
239
|
+
"""Reclaim *port* from a stale Tina4 dev server, only when it is safe.
|
|
272
240
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return False
|
|
279
|
-
try:
|
|
280
|
-
result = subprocess.run(
|
|
281
|
-
["lsof", "-ti", f":{port}"],
|
|
282
|
-
capture_output=True, text=True, timeout=5,
|
|
283
|
-
)
|
|
284
|
-
if result.returncode != 0 or not result.stdout.strip():
|
|
285
|
-
return False
|
|
241
|
+
Routes through the shared identity-checked takeover (TAKEOVER-DEC-01/02):
|
|
242
|
+
a holder is signalled ONLY when a Tina4 dev server recorded its PID in the
|
|
243
|
+
per-port PID file. A foreign holder is left running and a clear message is
|
|
244
|
+
printed; takeover is also skipped in a container, outside dev mode, and when
|
|
245
|
+
opted out (`TINA4_NO_TAKEOVER` / `tina4 serve --no-kill`).
|
|
286
246
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
os.kill(pid, signal.SIGTERM)
|
|
293
|
-
killed.append(str(pid))
|
|
294
|
-
except (ProcessLookupError, PermissionError):
|
|
295
|
-
pass
|
|
296
|
-
|
|
297
|
-
if not killed:
|
|
298
|
-
return False
|
|
299
|
-
import time
|
|
300
|
-
time.sleep(0.5)
|
|
301
|
-
print(f" ⚠ Killed existing process on port {port} (PID: {', '.join(killed)})")
|
|
247
|
+
Returns True only when a Tina4 holder was actually signalled.
|
|
248
|
+
"""
|
|
249
|
+
result = take_over_port(port, dev=is_dev(), no_takeover=no_takeover_opted_out())
|
|
250
|
+
if result.reclaimed:
|
|
251
|
+
print(f" ⚠ {result.message}")
|
|
302
252
|
return True
|
|
303
|
-
|
|
304
|
-
|
|
253
|
+
if result.refused and result.message:
|
|
254
|
+
print(f" {result.message}")
|
|
305
255
|
return False
|
|
306
256
|
|
|
307
257
|
|
|
@@ -834,7 +784,12 @@ def _serve(args):
|
|
|
834
784
|
# --no-reload flag
|
|
835
785
|
no_reload = "no-reload" in flags
|
|
836
786
|
|
|
837
|
-
#
|
|
787
|
+
# --no-kill opts out of port takeover for the whole process, so the CLI path
|
|
788
|
+
# here AND the runtime bind-failure fallback both honour it (TAKEOVER-DEC-03).
|
|
789
|
+
if "no-kill" in flags:
|
|
790
|
+
os.environ["TINA4_NO_TAKEOVER"] = "true"
|
|
791
|
+
|
|
792
|
+
# Reclaim the port from a stale Tina4 dev server only (identity-checked).
|
|
838
793
|
port = cli_port or int(os.environ.get("PORT", os.environ.get("TINA4_PORT", "7146")))
|
|
839
794
|
_kill_process_on_port(port)
|
|
840
795
|
|
|
@@ -907,14 +862,14 @@ def _migrate_status(args):
|
|
|
907
862
|
if completed:
|
|
908
863
|
print("\nCompleted migrations:")
|
|
909
864
|
for m in completed:
|
|
910
|
-
print(f" [batch {m['batch']}] {m['
|
|
865
|
+
print(f" [batch {m['batch']}] {m['migration_name']} ({m['executed_at']})")
|
|
911
866
|
else:
|
|
912
867
|
print("\nNo completed migrations.")
|
|
913
868
|
|
|
914
869
|
if pending:
|
|
915
870
|
print("\nPending migrations:")
|
|
916
871
|
for m in pending:
|
|
917
|
-
print(f" {m['
|
|
872
|
+
print(f" {m['migration_name']} ({m['description']})")
|
|
918
873
|
else:
|
|
919
874
|
print("\nNo pending migrations.")
|
|
920
875
|
|
|
@@ -977,10 +932,70 @@ def _routes(args):
|
|
|
977
932
|
print(f"\n{len(routes)} route(s) registered.")
|
|
978
933
|
|
|
979
934
|
|
|
935
|
+
def _run_inline_tests(root: Path) -> bool:
|
|
936
|
+
"""Discover ``@tests``-decorated functions under ``src/`` and run them.
|
|
937
|
+
|
|
938
|
+
Returns True if any inline test FAILED or ERRORED (so the caller can exit
|
|
939
|
+
non-zero), False if everything passed or there were no inline tests.
|
|
940
|
+
|
|
941
|
+
Only files whose text contains ``@tests`` are imported — a source file
|
|
942
|
+
without an inline test is never executed, so ``tina4 test`` cannot run a
|
|
943
|
+
scanned file's arbitrary side effect. This is the ``@tests`` decorator
|
|
944
|
+
surface wired to a real exit code (INLINE-DEC-01).
|
|
945
|
+
"""
|
|
946
|
+
import importlib.util
|
|
947
|
+
|
|
948
|
+
from tina4_python.Testing import reset, run_all
|
|
949
|
+
|
|
950
|
+
src = root / "src"
|
|
951
|
+
if not src.is_dir():
|
|
952
|
+
return False
|
|
953
|
+
|
|
954
|
+
reset()
|
|
955
|
+
discovered = 0
|
|
956
|
+
for path in sorted(src.rglob("*.py")):
|
|
957
|
+
try:
|
|
958
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
959
|
+
except OSError:
|
|
960
|
+
continue
|
|
961
|
+
if "@tests" not in text:
|
|
962
|
+
continue
|
|
963
|
+
discovered += 1
|
|
964
|
+
module_name = "tina4_inline_" + re.sub(r"\W", "_", str(path.relative_to(root)))
|
|
965
|
+
spec = importlib.util.spec_from_file_location(module_name, str(path))
|
|
966
|
+
if spec is None or spec.loader is None:
|
|
967
|
+
continue
|
|
968
|
+
try:
|
|
969
|
+
module = importlib.util.module_from_spec(spec)
|
|
970
|
+
spec.loader.exec_module(module)
|
|
971
|
+
except Exception as exc: # noqa: BLE001 — report and keep going
|
|
972
|
+
print(f" ! could not import {path.relative_to(root)}: {exc}")
|
|
973
|
+
|
|
974
|
+
if discovered == 0:
|
|
975
|
+
return False
|
|
976
|
+
|
|
977
|
+
results = run_all()
|
|
978
|
+
return (results["failed"] + results["errors"]) > 0
|
|
979
|
+
|
|
980
|
+
|
|
980
981
|
def _test(args):
|
|
981
|
-
"""Run the
|
|
982
|
-
|
|
983
|
-
|
|
982
|
+
"""Run the inline ``@tests`` suite AND the pytest suite; exit non-zero if either fails.
|
|
983
|
+
|
|
984
|
+
The inline stage discovers ``@tests``-decorated functions under ``src/`` and
|
|
985
|
+
runs them with a real exit code (INLINE-DEC-01) — the batteries-included flow
|
|
986
|
+
the docs advertise. The pytest stage still runs the ``tests/`` suite when that
|
|
987
|
+
directory exists (preserving the python#96 exit-code contract), so a project
|
|
988
|
+
that keeps its tests in ``tests/`` is unaffected.
|
|
989
|
+
"""
|
|
990
|
+
inline_failed = _run_inline_tests(Path.cwd())
|
|
991
|
+
|
|
992
|
+
pytest_code = 0
|
|
993
|
+
if Path("tests").is_dir():
|
|
994
|
+
pytest_code = subprocess.run(
|
|
995
|
+
[sys.executable, "-m", "pytest", "tests/"] + args
|
|
996
|
+
).returncode
|
|
997
|
+
|
|
998
|
+
sys.exit(1 if (inline_failed or pytest_code) else 0)
|
|
984
999
|
|
|
985
1000
|
|
|
986
1001
|
def _build(args):
|
|
@@ -1211,8 +1226,8 @@ def _gen_route(name: str, flags: dict, *, emit_test: bool = True):
|
|
|
1211
1226
|
@get("/api/{route_path}")
|
|
1212
1227
|
async def list_{route_path}(request, response):
|
|
1213
1228
|
"""List all {route_path} with pagination."""
|
|
1214
|
-
page = int(request.
|
|
1215
|
-
per_page = int(request.
|
|
1229
|
+
page = int(request.query.get("page", 1))
|
|
1230
|
+
per_page = int(request.query.get("per_page", 20))
|
|
1216
1231
|
offset = (page - 1) * per_page
|
|
1217
1232
|
records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
|
|
1218
1233
|
return response({{
|