tina4-python 3.13.89__tar.gz → 3.13.91__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.89 → tina4_python-3.13.91}/PKG-INFO +1 -1
- {tina4_python-3.13.89 → tina4_python-3.13.91}/pyproject.toml +1 -1
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/cli/__init__.py +5 -3
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/__init__.py +13 -3
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/metrics.py +67 -5
- tina4_python-3.13.91/tina4_python/dev_admin/metrics_engine.py +108 -0
- tina4_python-3.13.91/tina4_python/public/js/tina4-dev-admin.min.js +1127 -0
- tina4_python-3.13.89/tina4_python/public/js/tina4-dev-admin.min.js +0 -1127
- {tina4_python-3.13.89 → tina4_python-3.13.91}/.gitignore +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/README.md +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/env.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_pagination.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1766,7 +1766,9 @@ def _sample_literal(field_type: str) -> str:
|
|
|
1766
1766
|
|
|
1767
1767
|
def _emit_model_test(model: str, table: str, fields: list) -> None:
|
|
1768
1768
|
"""model → real SQLite roundtrip (create / read back / missing → None)."""
|
|
1769
|
-
|
|
1769
|
+
# Reuse the single DEFAULT_FIELDS constant rather than re-stating the literal,
|
|
1770
|
+
# so the co-emitted test can never describe a shape the model does not have.
|
|
1771
|
+
fields = fields or list(DEFAULT_FIELDS)
|
|
1770
1772
|
payload = ", ".join(f'"{fname}": {_sample_literal(ftype)}' for fname, ftype in fields)
|
|
1771
1773
|
# Assert a STRING field round-trips (type-safe); else just the id round-trips
|
|
1772
1774
|
# (avoids datetime/bool/float equality pitfalls on the read-back).
|
|
@@ -2615,7 +2617,7 @@ def _gen_form(name: str, flags: dict = None):
|
|
|
2615
2617
|
tina4python generate form Product --fields "name:string,price:float"
|
|
2616
2618
|
"""
|
|
2617
2619
|
flags = flags or {}
|
|
2618
|
-
fields =
|
|
2620
|
+
fields = _fields_or_default(flags.get("fields", ""))
|
|
2619
2621
|
table = _to_table(name)
|
|
2620
2622
|
route_name = table + "s"
|
|
2621
2623
|
|
|
@@ -2637,7 +2639,7 @@ def _gen_form(name: str, flags: dict = None):
|
|
|
2637
2639
|
|
|
2638
2640
|
# Build form fields
|
|
2639
2641
|
field_html = ""
|
|
2640
|
-
for fname, ftype in
|
|
2642
|
+
for fname, ftype in fields:
|
|
2641
2643
|
itype = input_types.get(ftype, "text")
|
|
2642
2644
|
label = fname.replace("_", " ").title()
|
|
2643
2645
|
step = ' step="0.01"' if ftype in ("float", "numeric", "decimal") else ""
|
|
@@ -2041,9 +2041,19 @@ async def _api_metrics(request, response):
|
|
|
2041
2041
|
|
|
2042
2042
|
|
|
2043
2043
|
async def _api_metrics_full(request, response):
|
|
2044
|
-
"""Full analysis —
|
|
2045
|
-
|
|
2046
|
-
|
|
2044
|
+
"""Full analysis — from the tina4 CLI metrics engine, else the local module.
|
|
2045
|
+
|
|
2046
|
+
The engine (ADR-0002) resolves imports to real file paths, so it is the only
|
|
2047
|
+
source with usable coupling data. It returns None when it is absent or too
|
|
2048
|
+
old to emit a field the dashboard renders, and we fall back rather than
|
|
2049
|
+
serve a payload with holes in it.
|
|
2050
|
+
"""
|
|
2051
|
+
from tina4_python.dev_admin.metrics import full_analysis, resolve_scan_target
|
|
2052
|
+
from tina4_python.dev_admin.metrics_engine import engine_analysis
|
|
2053
|
+
|
|
2054
|
+
root, scan_mode = resolve_scan_target()
|
|
2055
|
+
from_engine = engine_analysis(root, scan_mode)
|
|
2056
|
+
return response(from_engine if from_engine is not None else full_analysis())
|
|
2047
2057
|
|
|
2048
2058
|
|
|
2049
2059
|
async def _api_metrics_file(request, response):
|
|
@@ -23,6 +23,18 @@ _last_scan_root: str = ""
|
|
|
23
23
|
# ── Quick Metrics ──────────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _is_code_line(line: str) -> bool:
|
|
27
|
+
"""True for a line that counts toward LOC: not blank, not a comment.
|
|
28
|
+
|
|
29
|
+
The single definition of the rule. It used to be restated at each file-level
|
|
30
|
+
call site while function LOC ignored it entirely and returned a raw line
|
|
31
|
+
span, so `loc` meant two different things in one payload - see
|
|
32
|
+
_function_loc.
|
|
33
|
+
"""
|
|
34
|
+
stripped = line.strip()
|
|
35
|
+
return bool(stripped) and not stripped.startswith("#")
|
|
36
|
+
|
|
37
|
+
|
|
26
38
|
def _resolve_root(root: str = "src") -> str:
|
|
27
39
|
"""Pick the right directory to scan.
|
|
28
40
|
|
|
@@ -41,6 +53,23 @@ def _resolve_root(root: str = "src") -> str:
|
|
|
41
53
|
return framework_dir
|
|
42
54
|
|
|
43
55
|
|
|
56
|
+
def resolve_scan_target(root: str = "src") -> tuple[str, str]:
|
|
57
|
+
"""Return (directory to scan, scan_mode) for any metrics producer.
|
|
58
|
+
|
|
59
|
+
The CLI engine is language-agnostic and cannot know which directory holds a
|
|
60
|
+
framework package, so root resolution and the "framework" label stay here -
|
|
61
|
+
shared by this module and the engine adapter so the two never disagree about
|
|
62
|
+
what was measured.
|
|
63
|
+
"""
|
|
64
|
+
resolved = _resolve_root(root)
|
|
65
|
+
framework_dir = str(Path(__import__("tina4_python").__file__).parent)
|
|
66
|
+
resolved_path = Path(resolved)
|
|
67
|
+
scanning_framework = (
|
|
68
|
+
resolved_path == Path(framework_dir) or str(resolved_path).startswith(framework_dir)
|
|
69
|
+
)
|
|
70
|
+
return resolved, "framework" if scanning_framework else "project"
|
|
71
|
+
|
|
72
|
+
|
|
44
73
|
def quick_metrics(root: str = "src") -> dict:
|
|
45
74
|
"""Scan project files and return instant metrics."""
|
|
46
75
|
root = _resolve_root(root)
|
|
@@ -224,7 +253,7 @@ def full_analysis(root: str = "src") -> dict:
|
|
|
224
253
|
except ValueError:
|
|
225
254
|
rel_path = (str(f.relative_to(root_path.parent)) if root_path.parent != f else f.name).replace("\\", "/")
|
|
226
255
|
lines = source.splitlines()
|
|
227
|
-
loc = sum(1 for l in lines if
|
|
256
|
+
loc = sum(1 for l in lines if _is_code_line(l))
|
|
228
257
|
|
|
229
258
|
# Extract imports for coupling analysis
|
|
230
259
|
imports = _extract_imports(tree, rel_path)
|
|
@@ -491,7 +520,7 @@ def file_detail(file_path: str) -> dict:
|
|
|
491
520
|
|
|
492
521
|
functions.sort(key=lambda x: x["complexity"], reverse=True)
|
|
493
522
|
|
|
494
|
-
loc = sum(1 for l in lines if
|
|
523
|
+
loc = sum(1 for l in lines if _is_code_line(l))
|
|
495
524
|
classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
|
|
496
525
|
imports = _extract_imports(tree, file_path)
|
|
497
526
|
|
|
@@ -526,13 +555,37 @@ def file_detail(file_path: str) -> dict:
|
|
|
526
555
|
# ── AST Helpers ────────────────────────────────────────────────
|
|
527
556
|
|
|
528
557
|
|
|
558
|
+
def _own_body_nodes(node: ast.AST):
|
|
559
|
+
"""Yield the nodes belonging to this function's OWN body.
|
|
560
|
+
|
|
561
|
+
Nested function and class definitions are skipped: they are reported as
|
|
562
|
+
functions in their own right, so counting their decision points here too
|
|
563
|
+
would charge one branch to two different functions. That over-count grew
|
|
564
|
+
with nesting depth - an IIFE-style wrapper or a registrar that defines
|
|
565
|
+
twenty inner handlers absorbed the entire file's complexity and sat at the
|
|
566
|
+
top of the offenders list while the real hot spots hid below it.
|
|
567
|
+
|
|
568
|
+
A lambda is NOT skipped. Lambdas are never listed as separate functions, so
|
|
569
|
+
their decision points would simply vanish; they stay with the function that
|
|
570
|
+
encloses them.
|
|
571
|
+
"""
|
|
572
|
+
stack = list(ast.iter_child_nodes(node))
|
|
573
|
+
while stack:
|
|
574
|
+
child = stack.pop()
|
|
575
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
576
|
+
continue
|
|
577
|
+
yield child
|
|
578
|
+
stack.extend(ast.iter_child_nodes(child))
|
|
579
|
+
|
|
580
|
+
|
|
529
581
|
def _cyclomatic_complexity(node: ast.AST) -> int:
|
|
530
582
|
"""Calculate cyclomatic complexity for a function/method node.
|
|
531
583
|
|
|
532
584
|
CC = 1 + number of decision points (if/elif/for/while/except/and/or/assert)
|
|
585
|
+
in the function's own body. Nested functions are measured separately.
|
|
533
586
|
"""
|
|
534
587
|
cc = 1
|
|
535
|
-
for child in
|
|
588
|
+
for child in _own_body_nodes(node):
|
|
536
589
|
if isinstance(child, (ast.If, ast.IfExp)):
|
|
537
590
|
cc += 1
|
|
538
591
|
elif isinstance(child, ast.For):
|
|
@@ -553,9 +606,18 @@ def _cyclomatic_complexity(node: ast.AST) -> int:
|
|
|
553
606
|
|
|
554
607
|
|
|
555
608
|
def _function_loc(node: ast.AST, lines: list) -> int:
|
|
556
|
-
"""Count lines of
|
|
609
|
+
"""Count lines of CODE in a function, by the same rule as file-level LOC.
|
|
610
|
+
|
|
611
|
+
This returned `end_lineno - lineno + 1` - a raw line span - while the
|
|
612
|
+
docstring claimed lines of code and every file-level LOC excluded blanks and
|
|
613
|
+
comments. So a function's LOC and its file's LOC were different units sitting
|
|
614
|
+
side by side in the dashboard, and the maintainability index (which divides
|
|
615
|
+
by LOC) penalised a well-commented function for its comments.
|
|
616
|
+
"""
|
|
557
617
|
if hasattr(node, "end_lineno") and node.end_lineno:
|
|
558
|
-
|
|
618
|
+
span = lines[node.lineno - 1:node.end_lineno]
|
|
619
|
+
# Floor of 1: a one-line body must never report 0.
|
|
620
|
+
return max(1, sum(1 for l in span if _is_code_line(l)))
|
|
559
621
|
# Fallback: count indented lines
|
|
560
622
|
start = node.lineno - 1
|
|
561
623
|
count = 1
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Code metrics from the tina4 CLI engine (ADR-0002).
|
|
2
|
+
|
|
3
|
+
The metrics engine now lives in the Rust CLI: one tree-sitter implementation
|
|
4
|
+
covering every language, instead of a hand-rolled AST walker per framework. This
|
|
5
|
+
adapter shells out to it and reshapes the payload into the dashboard's contract.
|
|
6
|
+
|
|
7
|
+
It returns None whenever the CLI cannot supply a COMPLETE payload - no binary on
|
|
8
|
+
PATH, a non-zero exit, unparseable JSON, or an older CLI whose engine predates a
|
|
9
|
+
field the dashboard renders. The caller then falls back to the in-framework
|
|
10
|
+
module, so a stale CLI degrades quietly instead of blanking the chart.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# The dashboard reads these at the top level; the CLI nests them under "summary"
|
|
21
|
+
# because its own text report and the --fail-on gate group them there.
|
|
22
|
+
_SUMMARY_KEYS = (
|
|
23
|
+
"files_analyzed",
|
|
24
|
+
"total_functions",
|
|
25
|
+
"avg_complexity",
|
|
26
|
+
"avg_maintainability",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Fields the dashboard renders that a pre-3.8.59 engine does not emit. Checking
|
|
30
|
+
# for the DATA is honest where checking a version string is not: a user may run
|
|
31
|
+
# any CLI build, and the payload tells us what it can actually do.
|
|
32
|
+
_REQUIRED_FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "has_tests")
|
|
33
|
+
_REQUIRED_FUNCTION_KEYS = ("name", "file", "line", "complexity", "loc")
|
|
34
|
+
|
|
35
|
+
_TIMEOUT_SECONDS = 60
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def engine_path() -> str | None:
|
|
39
|
+
"""Absolute path to the tina4 CLI binary, or None when it is not installed."""
|
|
40
|
+
return shutil.which("tina4")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def engine_analysis(root: str, scan_mode: str = "project") -> dict | None:
|
|
44
|
+
"""Run `tina4 metrics --json` over root and adapt it to the dashboard shape.
|
|
45
|
+
|
|
46
|
+
The framework passes --path and owns scan_mode: it knows where its own
|
|
47
|
+
package lives, and the engine is deliberately language-agnostic, so root
|
|
48
|
+
resolution stays on this side of the boundary.
|
|
49
|
+
"""
|
|
50
|
+
binary = engine_path()
|
|
51
|
+
if binary is None:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
proc = subprocess.run(
|
|
56
|
+
[binary, "metrics", "--path", str(root), "--json"],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=_TIMEOUT_SECONDS,
|
|
60
|
+
)
|
|
61
|
+
except (OSError, subprocess.SubprocessError):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
if proc.returncode != 0 or not proc.stdout.strip():
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
payload = json.loads(proc.stdout)
|
|
69
|
+
except (json.JSONDecodeError, ValueError):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
return adapt_payload(payload, root, scan_mode)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def adapt_payload(payload: dict, root: str, scan_mode: str = "project") -> dict | None:
|
|
76
|
+
"""Flatten the engine payload into the dashboard contract, or None if short.
|
|
77
|
+
|
|
78
|
+
Split out from engine_analysis so the reshaping is testable without a
|
|
79
|
+
subprocess - the shaping is where the contract actually lives.
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(payload, dict):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
summary = payload.get("summary")
|
|
85
|
+
file_metrics = payload.get("file_metrics")
|
|
86
|
+
functions = payload.get("most_complex_functions")
|
|
87
|
+
if not isinstance(summary, dict) or not isinstance(file_metrics, list):
|
|
88
|
+
return None
|
|
89
|
+
if not isinstance(functions, list):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
if any(key not in summary for key in _SUMMARY_KEYS):
|
|
93
|
+
return None
|
|
94
|
+
if file_metrics and any(key not in file_metrics[0] for key in _REQUIRED_FILE_KEYS):
|
|
95
|
+
return None
|
|
96
|
+
if functions and any(key not in functions[0] for key in _REQUIRED_FUNCTION_KEYS):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
result = {key: summary[key] for key in _SUMMARY_KEYS}
|
|
100
|
+
result["file_metrics"] = file_metrics
|
|
101
|
+
result["most_complex_functions"] = functions[:15]
|
|
102
|
+
result["dependency_graph"] = payload.get("dependency_graph") or {}
|
|
103
|
+
# The framework decides these two - the engine always reports "project"
|
|
104
|
+
# because it cannot know which directory is a framework package.
|
|
105
|
+
result["scan_mode"] = scan_mode
|
|
106
|
+
result["scan_root"] = str(Path(root).resolve())
|
|
107
|
+
result["engine"] = "tina4-cli"
|
|
108
|
+
return result
|