tina4-python 3.13.119__tar.gz → 3.13.121__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.119 → tina4_python-3.13.121}/PKG-INFO +1 -1
- {tina4_python-3.13.119 → tina4_python-3.13.121}/pyproject.toml +1 -1
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/cli/__init__.py +300 -28
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/tools.py +93 -15
- {tina4_python-3.13.119 → tina4_python-3.13.121}/.gitignore +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/README.md +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/_import_helper.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/ai/client.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/port_takeover.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/database_url.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/sql_translator.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/env.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapter.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/arango.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/bolt.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/ultipa.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/graph_url.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/point.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/amqp_url.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/memcached_handler.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/sso.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -732,14 +732,35 @@ def _migrate(args):
|
|
|
732
732
|
|
|
733
733
|
|
|
734
734
|
def _migrate_create(args):
|
|
735
|
-
"""Create a new migration file.
|
|
735
|
+
"""Create a new migration file.
|
|
736
|
+
|
|
737
|
+
Thin delegation to `_emit_resolution("migration", ...)` — the same code
|
|
738
|
+
path `tina4python generate migration <desc>` takes — so both CLI surfaces
|
|
739
|
+
emit the SAME ADR-0063 `generate_v1_1` envelope, the SAME `tina4:edit`
|
|
740
|
+
markers in the file, and the SAME next-steps block. The only intentional
|
|
741
|
+
difference is that `migrate:create` never co-emits a test file (a private
|
|
742
|
+
`_no_test` marker on the flags dict suppresses the test-emit path in
|
|
743
|
+
`_gen_migration` and drops `test_paths[]` in the resolution), preserving
|
|
744
|
+
the "just a migration, no test" contract of `migrate:create`. Neither
|
|
745
|
+
surface is deprecated; both stay first-class producers of the envelope.
|
|
746
|
+
|
|
747
|
+
The Python API `create_migration()` in `tina4_python.migration.runner` is
|
|
748
|
+
unchanged and still callable directly.
|
|
749
|
+
"""
|
|
736
750
|
if not args:
|
|
737
751
|
print("Usage: tina4python migrate:create <description>")
|
|
738
752
|
sys.exit(1)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
753
|
+
# Split flags vs positional so `--json` / `--dry-run` flow through the
|
|
754
|
+
# envelope emitter (bare word args form the description).
|
|
755
|
+
flags, positional = _parse_flags(list(args))
|
|
756
|
+
if not positional:
|
|
757
|
+
print("Usage: tina4python migrate:create <description>")
|
|
758
|
+
sys.exit(1)
|
|
759
|
+
desc = " ".join(positional)
|
|
760
|
+
# Private marker read by _gen_migration + _resolve_generation to suppress
|
|
761
|
+
# the co-emitted test — see the `migrate:create` contract above.
|
|
762
|
+
flags["_no_test"] = True
|
|
763
|
+
_emit_resolution("migration", desc, flags)
|
|
743
764
|
|
|
744
765
|
|
|
745
766
|
def _migrate_rollback(args):
|
|
@@ -986,7 +1007,24 @@ _RESOLUTION_TARGETS = {"model", "route", "migration", "middleware"}
|
|
|
986
1007
|
|
|
987
1008
|
# Envelope schema version. Bump when the SHAPE changes; adding an optional
|
|
988
1009
|
# key on the resolution object is not a break.
|
|
989
|
-
|
|
1010
|
+
#
|
|
1011
|
+
# 1.1 (ADR-0063 Wave 1, 3.13.120): additive keys `edit_hints[]` and `next[]`
|
|
1012
|
+
# on `resolution`, plus `test_paths[]` (already emitted since v1) is now
|
|
1013
|
+
# surfaced in the human stderr block too. Every existing generator template
|
|
1014
|
+
# bakes `tina4:edit` markers at first-edit spots so a caller sees exactly
|
|
1015
|
+
# where to point a follow-up patch. Old envelope keys are preserved; a v1
|
|
1016
|
+
# consumer sees the v1 shape intact and simply ignores the new keys.
|
|
1017
|
+
_RESOLUTION_ENVELOPE_VERSION = "1.1"
|
|
1018
|
+
_RESOLUTION_ENVELOPE_NAME = "generate_v1_1"
|
|
1019
|
+
|
|
1020
|
+
# Marker syntax the generator templates bake in at first-edit spots — accepts
|
|
1021
|
+
# a Python (`#`), SQL (`--`), or Twig (`{#`) comment prefix, followed by a
|
|
1022
|
+
# short actionable label. The label ends at end-of-line or a closing Twig `#}`.
|
|
1023
|
+
# NEVER matches inside a string literal — the compiler regex is a plain textual
|
|
1024
|
+
# scan of freshly-written source, but no template contains a matching literal.
|
|
1025
|
+
_EDIT_MARKER_RE = re.compile(
|
|
1026
|
+
r"(?:#|--|\{#)\s*tina4:edit\s+([^\n#]+?)\s*(?:#\})?\s*$"
|
|
1027
|
+
)
|
|
990
1028
|
|
|
991
1029
|
|
|
992
1030
|
def _input_fields(flags: dict):
|
|
@@ -1002,6 +1040,118 @@ def _input_fields(flags: dict):
|
|
|
1002
1040
|
return value
|
|
1003
1041
|
|
|
1004
1042
|
|
|
1043
|
+
def _scan_edit_hints(root: Path, rel_paths: list[str]) -> list[dict]:
|
|
1044
|
+
"""Grep freshly-written files for `tina4:edit` markers (envelope v1.1).
|
|
1045
|
+
|
|
1046
|
+
Reads each `rel_paths` entry under `root`, walks its lines, and returns
|
|
1047
|
+
`[{file, line, label}]` — one entry per marker, in file+line order.
|
|
1048
|
+
|
|
1049
|
+
A missing file is silently skipped: the caller wires this into the
|
|
1050
|
+
envelope path where `actions_taken` is the source of truth for what
|
|
1051
|
+
landed. The scan is text-only — no imports, no parsing — so an edited
|
|
1052
|
+
template that fails to run still yields honest hints.
|
|
1053
|
+
"""
|
|
1054
|
+
hints: list[dict] = []
|
|
1055
|
+
for rel in rel_paths:
|
|
1056
|
+
f = root / rel
|
|
1057
|
+
if not f.is_file():
|
|
1058
|
+
continue
|
|
1059
|
+
try:
|
|
1060
|
+
text = f.read_text(encoding="utf-8")
|
|
1061
|
+
except OSError:
|
|
1062
|
+
continue
|
|
1063
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
1064
|
+
match = _EDIT_MARKER_RE.search(line)
|
|
1065
|
+
if match:
|
|
1066
|
+
hints.append({
|
|
1067
|
+
"file": rel,
|
|
1068
|
+
"line": lineno,
|
|
1069
|
+
"label": match.group(1).strip(),
|
|
1070
|
+
})
|
|
1071
|
+
return hints
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _hint_paths_from_resolution(target: str, resolution: dict) -> list[str]:
|
|
1075
|
+
"""The set of paths the caller should scan for `tina4:edit` markers.
|
|
1076
|
+
|
|
1077
|
+
Every path the generator ACTUALLY writes is worth scanning — the model
|
|
1078
|
+
file, the matching migration + down, the middleware file, the co-emitted
|
|
1079
|
+
test — because a marker in any of them names a first-edit spot the
|
|
1080
|
+
developer will hit. Order is stable: primary file, then migration + down,
|
|
1081
|
+
then any test paths declared in the envelope. The hints themselves land
|
|
1082
|
+
in file+line order regardless.
|
|
1083
|
+
"""
|
|
1084
|
+
paths: list[str] = []
|
|
1085
|
+
primary = resolution.get("file_path")
|
|
1086
|
+
if primary:
|
|
1087
|
+
paths.append(primary)
|
|
1088
|
+
|
|
1089
|
+
# Model + migration composite: the migration is written by _gen_migration
|
|
1090
|
+
# under the model's own generator, so its markers deserve to surface too.
|
|
1091
|
+
if target == "model" and resolution.get("migration_path"):
|
|
1092
|
+
migration = resolution["migration_path"]
|
|
1093
|
+
paths.append(migration)
|
|
1094
|
+
if migration.endswith(".sql"):
|
|
1095
|
+
paths.append(migration[:-len(".sql")] + ".down.sql")
|
|
1096
|
+
|
|
1097
|
+
# A bare migration also writes its own .down.sql.
|
|
1098
|
+
if target == "migration" and primary and primary.endswith(".sql"):
|
|
1099
|
+
paths.append(primary[:-len(".sql")] + ".down.sql")
|
|
1100
|
+
|
|
1101
|
+
# Co-emitted tests carry their own markers (see the test template).
|
|
1102
|
+
for test_path in resolution.get("test_paths", []):
|
|
1103
|
+
if test_path not in paths:
|
|
1104
|
+
paths.append(test_path)
|
|
1105
|
+
return paths
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _next_steps_for(target: str, name: str, resolution: dict, flags: dict) -> list[str]:
|
|
1109
|
+
"""Curated actionable next-steps a caller should see after generation.
|
|
1110
|
+
|
|
1111
|
+
Kept short and concrete: what to edit, what to run, how to try it. The
|
|
1112
|
+
entries are per-target because a generic "next steps" list dilutes into
|
|
1113
|
+
noise — a route's next step is `curl`, a middleware's next step is
|
|
1114
|
+
`@middleware(...)` on a real route.
|
|
1115
|
+
|
|
1116
|
+
Placeholders are resolved from the same shape the envelope already
|
|
1117
|
+
reports (class_name, table_name, routes), so a caller can trust the
|
|
1118
|
+
strings match what actually landed on disk.
|
|
1119
|
+
"""
|
|
1120
|
+
if target == "model":
|
|
1121
|
+
table = resolution.get("table_name") or _to_table(name)
|
|
1122
|
+
return [
|
|
1123
|
+
f"Edit src/orm/{name}.py to add fields beyond the default `name`",
|
|
1124
|
+
"Apply the migration: tina4 migrate",
|
|
1125
|
+
f"Scaffold CRUD around it: tina4 generate crud {name} --skip-model",
|
|
1126
|
+
f"Try it: tina4 serve -> curl http://localhost:7146/api/{table}",
|
|
1127
|
+
]
|
|
1128
|
+
|
|
1129
|
+
if target == "route":
|
|
1130
|
+
route_path = name.lstrip("/")
|
|
1131
|
+
first_url = (resolution.get("routes") or [f"/api/{route_path}"])[0]
|
|
1132
|
+
return [
|
|
1133
|
+
f"Edit src/routes/{route_path}.py to customise the handlers",
|
|
1134
|
+
"Start the server: tina4 serve",
|
|
1135
|
+
f"Try it: curl http://localhost:7146{first_url}",
|
|
1136
|
+
]
|
|
1137
|
+
|
|
1138
|
+
if target == "migration":
|
|
1139
|
+
return [
|
|
1140
|
+
"Apply it: tina4 migrate",
|
|
1141
|
+
"Roll back last batch: tina4 migrate --rollback",
|
|
1142
|
+
"Inspect the pending: tina4 migrate --status",
|
|
1143
|
+
]
|
|
1144
|
+
|
|
1145
|
+
if target == "middleware":
|
|
1146
|
+
snake = _to_snake(name)
|
|
1147
|
+
return [
|
|
1148
|
+
f"Register it on a route with @middleware({name}) in src/routes/",
|
|
1149
|
+
f"Run the co-emitted test: .venv/bin/python -m pytest tests/test_{snake}.py",
|
|
1150
|
+
]
|
|
1151
|
+
|
|
1152
|
+
return []
|
|
1153
|
+
|
|
1154
|
+
|
|
1005
1155
|
def _resolve_generation(target: str, name: str, flags: dict,
|
|
1006
1156
|
timestamp: str | None = None) -> dict:
|
|
1007
1157
|
"""Compute the resolution mapping for a generate call. Pure — no writes.
|
|
@@ -1060,6 +1210,10 @@ def _resolve_generation(target: str, name: str, flags: dict,
|
|
|
1060
1210
|
table = _to_snake(table)
|
|
1061
1211
|
filename = f"{ts}_{name}.sql"
|
|
1062
1212
|
is_create = name.startswith("create_")
|
|
1213
|
+
# Private `_no_test` marker (set by `migrate:create`, which is a
|
|
1214
|
+
# single-file operation): drop `test_paths[]` so the envelope
|
|
1215
|
+
# matches the on-disk effect. `_gen_migration` reads the same flag.
|
|
1216
|
+
want_test = is_create and not flags.get("_no_test")
|
|
1063
1217
|
return {
|
|
1064
1218
|
"class_name": None,
|
|
1065
1219
|
"table_name": table,
|
|
@@ -1067,7 +1221,7 @@ def _resolve_generation(target: str, name: str, flags: dict,
|
|
|
1067
1221
|
"migration_path": f"migrations/{filename}",
|
|
1068
1222
|
"transformations": [],
|
|
1069
1223
|
"routes": [],
|
|
1070
|
-
"test_paths": [f"tests/test_{table}_migration.py"] if
|
|
1224
|
+
"test_paths": [f"tests/test_{table}_migration.py"] if want_test else [],
|
|
1071
1225
|
}
|
|
1072
1226
|
|
|
1073
1227
|
if target == "middleware":
|
|
@@ -1112,12 +1266,19 @@ def _actions_from_resolution(target: str, resolution: dict) -> list[str]:
|
|
|
1112
1266
|
|
|
1113
1267
|
|
|
1114
1268
|
def _format_resolution_block(name: str, target: str, resolution: dict,
|
|
1115
|
-
dry_run: bool = False
|
|
1269
|
+
dry_run: bool = False,
|
|
1270
|
+
edit_hints: list[dict] | None = None,
|
|
1271
|
+
next_steps: list[str] | None = None) -> str:
|
|
1116
1272
|
"""Render the human-readable resolution block for stderr on bare calls.
|
|
1117
1273
|
|
|
1118
1274
|
Same shape as the plan / ADR-0062 example. A generator with no route/table
|
|
1119
1275
|
footprint (middleware, route without a matching table) simply omits the
|
|
1120
1276
|
lines that don't apply — the block is honest about what was chosen.
|
|
1277
|
+
|
|
1278
|
+
ADR-0063 (envelope v1.1) also surfaces `test_paths[]` (was in the envelope
|
|
1279
|
+
since v1, never printed), plus `edit_hints[]` under "Edit these lines:"
|
|
1280
|
+
and `next_steps[]` under "Next:". Each section omits when empty so the
|
|
1281
|
+
block never grows a header without content beneath it.
|
|
1121
1282
|
"""
|
|
1122
1283
|
suffix = " (dry-run — nothing written)" if dry_run else ""
|
|
1123
1284
|
lines = [f"Generated {target} {name}{suffix}"]
|
|
@@ -1145,6 +1306,13 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
|
|
|
1145
1306
|
if target != "migration" and resolution.get("migration_path"):
|
|
1146
1307
|
lines.append(f" migration {resolution['migration_path']}")
|
|
1147
1308
|
|
|
1309
|
+
# ADR-0063: surface test_paths[] in the human block too (it has been in the
|
|
1310
|
+
# envelope since v1 but only the JSON path printed it). Skipped when empty
|
|
1311
|
+
# so a migration without a test file doesn't grow an empty "tests" row.
|
|
1312
|
+
test_paths = resolution.get("test_paths") or []
|
|
1313
|
+
if test_paths:
|
|
1314
|
+
lines.append(f" tests {', '.join(test_paths)}")
|
|
1315
|
+
|
|
1148
1316
|
if pluralize:
|
|
1149
1317
|
lines.append("")
|
|
1150
1318
|
lines.append(f" To keep the raw name '{pluralize['from']}' as the table:")
|
|
@@ -1154,21 +1322,47 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
|
|
|
1154
1322
|
"(opt-in, ADR-0062 forthcoming)"
|
|
1155
1323
|
)
|
|
1156
1324
|
|
|
1325
|
+
# ADR-0063 additive sections. Each renders only when there is content, so
|
|
1326
|
+
# a target with no markers or no next-steps stays as tight as before.
|
|
1327
|
+
if edit_hints:
|
|
1328
|
+
lines.append("")
|
|
1329
|
+
lines.append("Edit these lines:")
|
|
1330
|
+
# Left-pad the file:line column so labels align — makes scanning easier
|
|
1331
|
+
# when there are three or four hints of different path lengths.
|
|
1332
|
+
addrs = [f"{h['file']}:{h['line']}" for h in edit_hints]
|
|
1333
|
+
width = max(len(a) for a in addrs)
|
|
1334
|
+
for addr, hint in zip(addrs, edit_hints):
|
|
1335
|
+
lines.append(f" {addr:<{width}} {hint['label']}")
|
|
1336
|
+
|
|
1337
|
+
if next_steps:
|
|
1338
|
+
lines.append("")
|
|
1339
|
+
lines.append("Next:")
|
|
1340
|
+
for i, step in enumerate(next_steps, start=1):
|
|
1341
|
+
lines.append(f" {i}. {step}")
|
|
1342
|
+
|
|
1157
1343
|
return "\n".join(lines) + "\n"
|
|
1158
1344
|
|
|
1159
1345
|
|
|
1160
1346
|
def _emit_resolution(target: str, name: str, flags: dict) -> None:
|
|
1161
1347
|
"""Handle the --json / --dry-run / bare paths for the 4 wired generators.
|
|
1162
1348
|
|
|
1163
|
-
Contract shared by every language (ADR-0062):
|
|
1349
|
+
Contract shared by every language (ADR-0062, extended by ADR-0063):
|
|
1164
1350
|
- --json → envelope on stdout; nothing else on stdout.
|
|
1165
1351
|
- --dry-run → skip every write; envelope with dry_run=true.
|
|
1166
1352
|
- --json + --dry-run → both compose (envelope with actions_taken=[]).
|
|
1167
1353
|
- bare → normal write, then human resolution block on stderr.
|
|
1354
|
+
|
|
1355
|
+
Envelope v1.1 (ADR-0063) additively surfaces `resolution.edit_hints[]`
|
|
1356
|
+
(one per `tina4:edit` marker found in freshly-written files) and
|
|
1357
|
+
`resolution.next[]` (curated actionable next-steps per verb). The
|
|
1358
|
+
hints are scanned from disk on the write paths; on --dry-run the
|
|
1359
|
+
generator runs in a throw-away tmpdir so the same scan gives the
|
|
1360
|
+
caller a real preview without touching the working tree.
|
|
1168
1361
|
"""
|
|
1169
1362
|
import contextlib
|
|
1170
1363
|
import io
|
|
1171
1364
|
import json as _json
|
|
1365
|
+
import tempfile
|
|
1172
1366
|
|
|
1173
1367
|
dry_run = bool(flags.get("dry-run"))
|
|
1174
1368
|
want_json = bool(flags.get("json"))
|
|
@@ -1178,21 +1372,55 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
|
|
|
1178
1372
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
1179
1373
|
resolution = _resolve_generation(target, name, flags, timestamp=timestamp)
|
|
1180
1374
|
|
|
1375
|
+
handler = GENERATORS[target]["handler"]
|
|
1376
|
+
generator_flags = dict(flags)
|
|
1377
|
+
generator_flags["_timestamp"] = timestamp
|
|
1378
|
+
# `_gen_*` handlers read the primary flags; `--dry-run` / `--json` are
|
|
1379
|
+
# envelope-only concerns, they must not leak into the underlying writer.
|
|
1380
|
+
generator_flags.pop("dry-run", None)
|
|
1381
|
+
generator_flags.pop("json", None)
|
|
1382
|
+
|
|
1181
1383
|
actions_taken: list[str] = []
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1384
|
+
edit_hints: list[dict] = []
|
|
1385
|
+
|
|
1386
|
+
hint_paths = _hint_paths_from_resolution(target, resolution)
|
|
1387
|
+
|
|
1388
|
+
if dry_run:
|
|
1389
|
+
# Render into a throw-away tree so the scan sees the SAME bytes the
|
|
1390
|
+
# writer would drop on disk. The envelope's paths still point at the
|
|
1391
|
+
# real project paths (unchanged by the tmpdir chdir) — the caller
|
|
1392
|
+
# can trust `edit_hints[].file` and `resolution.file_path` share a
|
|
1393
|
+
# coordinate space.
|
|
1394
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1395
|
+
original_cwd = os.getcwd()
|
|
1396
|
+
os.chdir(tmp)
|
|
1397
|
+
try:
|
|
1398
|
+
# Suppress the generator's own "Created X" lines — this is a
|
|
1399
|
+
# dry-run, and its "creations" happen in a tmpdir the caller
|
|
1400
|
+
# will never see. `--json` also swallows stdout on the real
|
|
1401
|
+
# write path; this keeps the two paths symmetrical.
|
|
1402
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1403
|
+
handler(name, generator_flags)
|
|
1404
|
+
edit_hints = _scan_edit_hints(Path(tmp), hint_paths)
|
|
1405
|
+
finally:
|
|
1406
|
+
os.chdir(original_cwd)
|
|
1407
|
+
else:
|
|
1408
|
+
# Real write. When --json we swallow the generator's stdout so the
|
|
1185
1409
|
# JSON envelope is the SOLE thing on stdout; otherwise we let its
|
|
1186
1410
|
# "Created X" lines through as before.
|
|
1187
|
-
generator_flags = dict(flags)
|
|
1188
|
-
generator_flags["_timestamp"] = timestamp
|
|
1189
|
-
handler = GENERATORS[target]["handler"]
|
|
1190
|
-
|
|
1191
1411
|
stdout_sink = io.StringIO() if want_json else sys.stdout
|
|
1192
1412
|
with contextlib.redirect_stdout(stdout_sink):
|
|
1193
1413
|
handler(name, generator_flags)
|
|
1194
1414
|
|
|
1195
1415
|
actions_taken = _actions_from_resolution(target, resolution)
|
|
1416
|
+
edit_hints = _scan_edit_hints(Path.cwd(), hint_paths)
|
|
1417
|
+
|
|
1418
|
+
# ADR-0063 additive envelope keys — computed once and reused by the JSON
|
|
1419
|
+
# and human paths so what the caller sees on stderr matches the envelope
|
|
1420
|
+
# byte-for-byte.
|
|
1421
|
+
next_steps = _next_steps_for(target, name, resolution, flags)
|
|
1422
|
+
resolution["edit_hints"] = edit_hints
|
|
1423
|
+
resolution["next"] = next_steps
|
|
1196
1424
|
|
|
1197
1425
|
envelope = {
|
|
1198
1426
|
"command": "generate",
|
|
@@ -1209,8 +1437,12 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
|
|
|
1209
1437
|
# Human resolution block always lands on STDERR (per plan): stdout
|
|
1210
1438
|
# keeps whatever the generator printed on the bare path, and the
|
|
1211
1439
|
# block never contends with a caller parsing stdout.
|
|
1212
|
-
sys.stderr.write(_format_resolution_block(
|
|
1213
|
-
|
|
1440
|
+
sys.stderr.write(_format_resolution_block(
|
|
1441
|
+
name, target, resolution,
|
|
1442
|
+
dry_run=dry_run,
|
|
1443
|
+
edit_hints=edit_hints,
|
|
1444
|
+
next_steps=next_steps,
|
|
1445
|
+
))
|
|
1214
1446
|
|
|
1215
1447
|
|
|
1216
1448
|
# ── Generate (rich scaffolding) ───────────────────────────────────────
|
|
@@ -1294,11 +1526,18 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
|
|
|
1294
1526
|
|
|
1295
1527
|
imports = ", ".join(sorted(used_types))
|
|
1296
1528
|
|
|
1297
|
-
# Build field lines
|
|
1298
|
-
|
|
1529
|
+
# Build field lines. Bracketed by two `tina4:edit` markers (ADR-0063) so
|
|
1530
|
+
# a first-time user sees WHERE to add columns and relationships — the
|
|
1531
|
+
# `id` and `created_at` lines are framework-managed, the block between
|
|
1532
|
+
# is theirs to grow.
|
|
1533
|
+
field_lines = [
|
|
1534
|
+
" id = IntegerField(primary_key=True, auto_increment=True)",
|
|
1535
|
+
" # tina4:edit add fields here",
|
|
1536
|
+
]
|
|
1299
1537
|
for fname, ftype in fields:
|
|
1300
1538
|
info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
|
|
1301
1539
|
field_lines.append(f" {fname} = {info['orm']}()")
|
|
1540
|
+
field_lines.append(" # tina4:edit add relationships here (e.g. author = ForeignKeyField(to=Author))")
|
|
1302
1541
|
field_lines.append(" created_at = DateTimeField()")
|
|
1303
1542
|
|
|
1304
1543
|
# Write model file
|
|
@@ -1395,6 +1634,7 @@ async def list_{route_path}(request, response):
|
|
|
1395
1634
|
per_page = int(request.query.get("per_page", 20))
|
|
1396
1635
|
offset = (page - 1) * per_page
|
|
1397
1636
|
records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
|
|
1637
|
+
# tina4:edit customise the list projection (filters, ordering, fields)
|
|
1398
1638
|
return response({{
|
|
1399
1639
|
"records": [r.to_dict() for r in records],
|
|
1400
1640
|
"count": total,
|
|
@@ -1588,6 +1828,7 @@ def _gen_crud(name: str, flags: dict):
|
|
|
1588
1828
|
'{% block content %}\n'
|
|
1589
1829
|
'<div class="container mt-4">\n'
|
|
1590
1830
|
f' <h1>{name}s</h1>\n'
|
|
1831
|
+
' {# tina4:edit restrict fields exposed to the API here #}\n'
|
|
1591
1832
|
' <table class="table">\n'
|
|
1592
1833
|
' <thead>\n'
|
|
1593
1834
|
' <tr>\n'
|
|
@@ -1646,6 +1887,12 @@ def _gen_migration(name: str, flags: dict = None, *,
|
|
|
1646
1887
|
# snapshotted timestamp; if it was supplied via the private `_timestamp`
|
|
1647
1888
|
# flag, honour it so envelope and disk agree byte-for-byte.
|
|
1648
1889
|
timestamp = flags.get("_timestamp") or now.strftime("%Y%m%d%H%M%S")
|
|
1890
|
+
# Private `_no_test` marker (set by `migrate:create`, which is a
|
|
1891
|
+
# single-file operation): suppress the co-emitted test regardless of the
|
|
1892
|
+
# keyword default. `_resolve_generation` reads the same marker so the
|
|
1893
|
+
# envelope's `test_paths[]` and the on-disk effect stay in agreement.
|
|
1894
|
+
if flags.get("_no_test"):
|
|
1895
|
+
emit_test = False
|
|
1649
1896
|
target = Path("migrations")
|
|
1650
1897
|
target.mkdir(parents=True, exist_ok=True)
|
|
1651
1898
|
|
|
@@ -1671,11 +1918,21 @@ def _gen_migration(name: str, flags: dict = None, *,
|
|
|
1671
1918
|
col_lines.append(f" {fname} {info['sql']}{default}")
|
|
1672
1919
|
col_lines.append(" created_at TEXT DEFAULT CURRENT_TIMESTAMP")
|
|
1673
1920
|
|
|
1674
|
-
|
|
1921
|
+
# ADR-0063: `tina4:edit` marker between the last column and the closing
|
|
1922
|
+
# paren, placed OUTSIDE the comma-joined column list so the SQL stays
|
|
1923
|
+
# valid. Points a first-time user at where to add columns beyond the
|
|
1924
|
+
# framework-managed id + created_at.
|
|
1925
|
+
up_sql = (
|
|
1926
|
+
f"CREATE TABLE IF NOT EXISTS {table} (\n"
|
|
1927
|
+
+ ",\n".join(col_lines)
|
|
1928
|
+
+ "\n -- tina4:edit add columns beyond id + created_at\n);"
|
|
1929
|
+
)
|
|
1675
1930
|
down_sql = f"DROP TABLE IF EXISTS {table};"
|
|
1676
1931
|
else:
|
|
1677
|
-
up_sql = f"--
|
|
1678
|
-
|
|
1932
|
+
up_sql = (f"-- tina4:edit write the UP migration SQL\n"
|
|
1933
|
+
f"-- Example: ALTER TABLE {table} ADD COLUMN new_col TEXT DEFAULT '';")
|
|
1934
|
+
down_sql = (f"-- tina4:edit write the DOWN rollback SQL\n"
|
|
1935
|
+
f"-- Example: ALTER TABLE {table} DROP COLUMN new_col;")
|
|
1679
1936
|
|
|
1680
1937
|
content = (
|
|
1681
1938
|
f"-- Migration: {name}\n"
|
|
@@ -1739,12 +1996,14 @@ class {name}:
|
|
|
1739
1996
|
Return (request, response) to continue, or
|
|
1740
1997
|
return (request, response("error", 401)) to block.
|
|
1741
1998
|
"""
|
|
1999
|
+
# tina4:edit guard the request here (auth check, rate limit, headers)
|
|
1742
2000
|
Log.info(f"{name}: {{request.method}} {{request.url}}")
|
|
1743
2001
|
return request, response
|
|
1744
2002
|
|
|
1745
2003
|
@staticmethod
|
|
1746
2004
|
def after_{snake}(request, response):
|
|
1747
2005
|
"""Runs after the route handler."""
|
|
2006
|
+
# tina4:edit inject headers or audit the response here
|
|
1748
2007
|
return request, response
|
|
1749
2008
|
'''
|
|
1750
2009
|
path.write_text(content, encoding="utf-8")
|
|
@@ -1861,7 +2120,7 @@ class Test{model}:
|
|
|
1861
2120
|
|
|
1862
2121
|
def test_list_{snake}(self):
|
|
1863
2122
|
"""Test listing {snake}."""
|
|
1864
|
-
#
|
|
2123
|
+
# tina4:edit add assertions here (real DB, real request — no mocks)
|
|
1865
2124
|
assert True
|
|
1866
2125
|
|
|
1867
2126
|
def test_get_{singular}(self):
|
|
@@ -1902,6 +2161,7 @@ class Test{name.title().replace("_", "")}:
|
|
|
1902
2161
|
|
|
1903
2162
|
def test_example(self):
|
|
1904
2163
|
"""Example test — replace with real tests."""
|
|
2164
|
+
# tina4:edit add assertions here (real DB, real request — no mocks)
|
|
1905
2165
|
assert True
|
|
1906
2166
|
'''
|
|
1907
2167
|
|
|
@@ -2463,6 +2723,7 @@ def _gen_service(name: str, flags: dict = None):
|
|
|
2463
2723
|
' """The scheduled task body. `context` is a ServiceContext with\n'
|
|
2464
2724
|
" .name, .stop_event and .log; a daemon-style task would loop until\n"
|
|
2465
2725
|
' context.stop_event.is_set()."""\n'
|
|
2726
|
+
" # tina4:edit main service loop here\n"
|
|
2466
2727
|
"__BODY__"
|
|
2467
2728
|
"\n\n"
|
|
2468
2729
|
"def register(runner: ServiceRunner) -> None:\n"
|
|
@@ -2534,6 +2795,7 @@ def _gen_queue(name: str, flags: dict = None):
|
|
|
2534
2795
|
"\n"
|
|
2535
2796
|
"def handle___SLUG__(data: dict):\n"
|
|
2536
2797
|
' """Process ONE __TOPIC__ job payload (`data` is Job.data — the pushed dict)."""\n'
|
|
2798
|
+
" # tina4:edit handle the job payload here\n"
|
|
2537
2799
|
"__BODY__"
|
|
2538
2800
|
"\n\n"
|
|
2539
2801
|
"def consume___SLUG__(context=None):\n"
|
|
@@ -2612,6 +2874,7 @@ def _gen_validator(name: str, flags: dict = None):
|
|
|
2612
2874
|
' return response.error("VALIDATION_FAILED", v.errors()[0]["message"], 400)\n'
|
|
2613
2875
|
' """\n'
|
|
2614
2876
|
" validator = Validator(data)\n"
|
|
2877
|
+
" # tina4:edit add validation rules here\n"
|
|
2615
2878
|
"__BODY__"
|
|
2616
2879
|
" return validator\n"
|
|
2617
2880
|
)
|
|
@@ -2664,6 +2927,7 @@ def _gen_seeder(name: str, flags: dict = None):
|
|
|
2664
2927
|
" need a specific shape here. Each callable receives a FakeData instance:\n"
|
|
2665
2928
|
' return {"email": lambda fake: fake.email(), "status": "active"}\n'
|
|
2666
2929
|
' """\n'
|
|
2930
|
+
" # tina4:edit add seed data here\n"
|
|
2667
2931
|
"__BODY__"
|
|
2668
2932
|
"\n\n"
|
|
2669
2933
|
"def run(db):\n"
|
|
@@ -2730,6 +2994,7 @@ def _gen_websocket(name: str, flags: dict = None):
|
|
|
2730
2994
|
' if event == "close":\n'
|
|
2731
2995
|
" return\n"
|
|
2732
2996
|
' # event == "message"\n'
|
|
2997
|
+
" # tina4:edit handle inbound messages here\n"
|
|
2733
2998
|
"__BODY__"
|
|
2734
2999
|
)
|
|
2735
3000
|
content = (
|
|
@@ -2781,6 +3046,7 @@ def _gen_listener(name: str, flags: dict = None):
|
|
|
2781
3046
|
'@on("__EVENT__")\n'
|
|
2782
3047
|
"def on___SLUG__(data=None):\n"
|
|
2783
3048
|
" \"\"\"React to '__EVENT__'. `data` is whatever emit('__EVENT__', data) passed.\"\"\"\n"
|
|
3049
|
+
" # tina4:edit react to the event here\n"
|
|
2784
3050
|
"__BODY__"
|
|
2785
3051
|
)
|
|
2786
3052
|
content = (
|
|
@@ -2866,6 +3132,7 @@ def _gen_form(name: str, flags: dict = None):
|
|
|
2866
3132
|
' <h1>{%% if item.id %%}Edit %s{%% else %%}Create %s{%% endif %%}</h1>\n'
|
|
2867
3133
|
' <form method="post" action="/api/%s{%% if item.id %%}/{{ item.id }}{%% endif %%}">\n'
|
|
2868
3134
|
' {{ form_token() }}\n'
|
|
3135
|
+
' {# tina4:edit customise the form fields here #}\n'
|
|
2869
3136
|
'%s'
|
|
2870
3137
|
' <button type="submit" class="btn btn-primary">\n'
|
|
2871
3138
|
' {%% if item.id %%}Update{%% else %%}Create{%% endif %%}\n'
|
|
@@ -2911,6 +3178,7 @@ def _gen_view(name: str, flags: dict = None):
|
|
|
2911
3178
|
' <h1>%s</h1>\n'
|
|
2912
3179
|
' <a href="/%s/create" class="btn btn-primary">Add %s</a>\n'
|
|
2913
3180
|
' </div>\n'
|
|
3181
|
+
' {# tina4:edit customise the list columns or add filters here #}\n'
|
|
2914
3182
|
' <table class="table">\n'
|
|
2915
3183
|
' <thead>\n'
|
|
2916
3184
|
' <tr>\n'
|
|
@@ -2959,6 +3227,7 @@ def _gen_view(name: str, flags: dict = None):
|
|
|
2959
3227
|
' <a href="/%s" class="btn btn-outline-secondary">Back</a>\n'
|
|
2960
3228
|
' </div>\n'
|
|
2961
3229
|
' </div>\n'
|
|
3230
|
+
' {# tina4:edit arrange the detail fields here #}\n'
|
|
2962
3231
|
'%s\n'
|
|
2963
3232
|
'</div>\n'
|
|
2964
3233
|
'{%% endblock %%}\n'
|
|
@@ -3009,6 +3278,7 @@ def _gen_auth(name: str = None, flags: dict = None):
|
|
|
3009
3278
|
' user = User.create({\n'
|
|
3010
3279
|
' "email": email,\n'
|
|
3011
3280
|
' "password": Auth.hash_password(password),\n'
|
|
3281
|
+
' # tina4:edit add roles/permissions here\n'
|
|
3012
3282
|
' "role": "user",\n'
|
|
3013
3283
|
' })\n'
|
|
3014
3284
|
' return response({"message": "Registered", "id": user.id}, 201)\n\n\n'
|
|
@@ -3375,12 +3645,14 @@ def _commands_manifest() -> dict:
|
|
|
3375
3645
|
"framework": "python",
|
|
3376
3646
|
"version": __version__,
|
|
3377
3647
|
"commands": commands,
|
|
3378
|
-
# ADR-0062: agents discover the generate resolution envelope
|
|
3379
|
-
# programmatically. A version bump here is a break; adding an
|
|
3380
|
-
# optional envelope key is not.
|
|
3648
|
+
# ADR-0062 / ADR-0063: agents discover the generate resolution envelope
|
|
3649
|
+
# shape programmatically. A version bump here is a break; adding an
|
|
3650
|
+
# optional envelope key is not. 1.1 (envelope generate_v1_1) added
|
|
3651
|
+
# `edit_hints[]` and `next[]` on the resolution — every v1 key
|
|
3652
|
+
# preserved, so a v1 consumer keeps working unchanged.
|
|
3381
3653
|
"resolution_contract": {
|
|
3382
3654
|
"version": _RESOLUTION_ENVELOPE_VERSION,
|
|
3383
|
-
"envelope":
|
|
3655
|
+
"envelope": _RESOLUTION_ENVELOPE_NAME,
|
|
3384
3656
|
},
|
|
3385
3657
|
}
|
|
3386
3658
|
|