tina4-python 3.11.14__tar.gz → 3.11.17__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.11.14 → tina4_python-3.11.17}/PKG-INFO +1 -1
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/server.py +32 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/debug/__init__.py +6 -3
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dev_admin/__init__.py +190 -0
- tina4_python-3.11.17/tina4_python/dev_admin/plan.py +454 -0
- tina4_python-3.11.17/tina4_python/dev_admin/project_index.py +417 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/mcp/__init__.py +19 -0
- tina4_python-3.11.17/tina4_python/mcp/tools.py +742 -0
- tina4_python-3.11.17/tina4_python/public/js/tina4-dev-admin.js +1279 -0
- tina4_python-3.11.17/tina4_python/public/js/tina4-dev-admin.min.js +1279 -0
- tina4_python-3.11.14/tina4_python/mcp/tools.py +0 -348
- tina4_python-3.11.14/tina4_python/public/js/tina4-dev-admin.js +0 -565
- tina4_python-3.11.14/tina4_python/public/js/tina4-dev-admin.min.js +0 -565
- {tina4_python-3.11.14 → tina4_python-3.11.17}/.gitignore +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/README.md +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/pyproject.toml +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/router.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1615,6 +1615,38 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1615
1615
|
log_level = os.environ.get("TINA4_LOG_LEVEL", "error" if not is_production else "error")
|
|
1616
1616
|
Log.configure(level=log_level, production=is_production)
|
|
1617
1617
|
|
|
1618
|
+
# Install a top-level exception hook so uncaught exceptions bubbling
|
|
1619
|
+
# out of anything (a route handler, a background task, the event
|
|
1620
|
+
# loop itself on startup) land in logs/error.log. Without this,
|
|
1621
|
+
# an uncaught exception surfaces only via Python's default stderr
|
|
1622
|
+
# writer and never touches Log — the same gap PHP had before its
|
|
1623
|
+
# set_exception_handler fix. Chains to the previous hook so any
|
|
1624
|
+
# debugger / IDE hook already in place still fires.
|
|
1625
|
+
import sys as _sys
|
|
1626
|
+
import traceback as _traceback
|
|
1627
|
+
_prior_excepthook = _sys.excepthook
|
|
1628
|
+
|
|
1629
|
+
def _tina4_excepthook(exc_type, exc_value, exc_tb):
|
|
1630
|
+
# KeyboardInterrupt is a user-initiated Ctrl+C, not an error —
|
|
1631
|
+
# defer to the prior hook (which prints a clean traceback).
|
|
1632
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
1633
|
+
_prior_excepthook(exc_type, exc_value, exc_tb)
|
|
1634
|
+
return
|
|
1635
|
+
try:
|
|
1636
|
+
trace_text = "".join(_traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
1637
|
+
Log.error(
|
|
1638
|
+
f"Uncaught {exc_type.__name__}: {exc_value}",
|
|
1639
|
+
trace=trace_text,
|
|
1640
|
+
)
|
|
1641
|
+
except Exception:
|
|
1642
|
+
# If logging itself fails (disk full, permissions, logger
|
|
1643
|
+
# not initialised yet), fall through to the prior hook so
|
|
1644
|
+
# the user still sees something in stderr.
|
|
1645
|
+
pass
|
|
1646
|
+
_prior_excepthook(exc_type, exc_value, exc_tb)
|
|
1647
|
+
|
|
1648
|
+
_sys.excepthook = _tina4_excepthook
|
|
1649
|
+
|
|
1618
1650
|
# Ensure folders
|
|
1619
1651
|
_ensure_folders()
|
|
1620
1652
|
|
|
@@ -200,12 +200,15 @@ class Log:
|
|
|
200
200
|
color = cls.COLORS.get(level, "")
|
|
201
201
|
print(f"{color}{line}{cls.RESET}")
|
|
202
202
|
|
|
203
|
-
# Always write ALL levels to file (raw log, no filtering)
|
|
203
|
+
# Always write ALL levels to the main file (raw log, no filtering)
|
|
204
204
|
if cls._writer:
|
|
205
205
|
cls._writer.write(line)
|
|
206
206
|
|
|
207
|
-
#
|
|
208
|
-
|
|
207
|
+
# Mirror WARNING and ERROR into the dedicated error log so
|
|
208
|
+
# `tail -f logs/error.log` gives just the stuff worth looking
|
|
209
|
+
# at, without wading through DEBUG / INFO noise. Parity with
|
|
210
|
+
# tina4-php's Log class.
|
|
211
|
+
if cls._error_writer and cls.LEVELS.get(level, 0) >= cls.LEVELS["warning"]:
|
|
209
212
|
cls._error_writer.write(line)
|
|
210
213
|
|
|
211
214
|
@classmethod
|
|
@@ -348,6 +348,20 @@ def get_api_handlers() -> dict:
|
|
|
348
348
|
"/__dev/api/deps/search": ("GET", _api_deps_search),
|
|
349
349
|
"/__dev/api/deps/install": ("POST", _api_deps_install),
|
|
350
350
|
"/__dev/api/git/status": ("GET", _api_git_status),
|
|
351
|
+
# ── MCP REST shim ──
|
|
352
|
+
# Dev-admin speaks a REST flavour of MCP (plain GET/POST with
|
|
353
|
+
# JSON bodies) rather than the JSON-RPC SSE protocol used by
|
|
354
|
+
# Claude Desktop et al. Both surfaces share the same
|
|
355
|
+
# `_default_server` tool registry, so tools registered via the
|
|
356
|
+
# @mcp_tool decorator appear in both immediately.
|
|
357
|
+
"/__dev/api/mcp/tools": ("GET", _api_mcp_tools),
|
|
358
|
+
"/__dev/api/mcp/call": ("POST", _api_mcp_call),
|
|
359
|
+
# ── Scaffold REST shim ──
|
|
360
|
+
# Wraps the tina4python CLI's `generate <kind> <name>` so the
|
|
361
|
+
# + Route / + Model / + Migration / + Middleware buttons work
|
|
362
|
+
# without shelling out from the browser.
|
|
363
|
+
"/__dev/api/scaffold": ("GET", _api_scaffold_list),
|
|
364
|
+
"/__dev/api/scaffold/run": ("POST", _api_scaffold_run),
|
|
351
365
|
}
|
|
352
366
|
|
|
353
367
|
|
|
@@ -2056,5 +2070,181 @@ async def _api_git_status(request, response):
|
|
|
2056
2070
|
return response(result)
|
|
2057
2071
|
|
|
2058
2072
|
|
|
2073
|
+
# ─── MCP REST shim ─────────────────────────────────────────────────
|
|
2074
|
+
#
|
|
2075
|
+
# Exposes the framework's MCP tool registry (`_default_server`) over
|
|
2076
|
+
# plain GET/POST JSON so the dev-admin browser panel and any other
|
|
2077
|
+
# REST client can enumerate and invoke tools without speaking the full
|
|
2078
|
+
# JSON-RPC 2.0 over SSE protocol.
|
|
2079
|
+
#
|
|
2080
|
+
# The JSON-RPC endpoint at `/__dev/mcp/{message,sse}` stays live for
|
|
2081
|
+
# proper MCP clients (Claude Desktop et al.) — the two surfaces share
|
|
2082
|
+
# the same registry, so tools registered via the `@mcp_tool` decorator
|
|
2083
|
+
# show up on both immediately.
|
|
2084
|
+
|
|
2085
|
+
async def _api_mcp_tools(request, response):
|
|
2086
|
+
"""GET — return the MCP tool registry as a plain JSON list.
|
|
2087
|
+
|
|
2088
|
+
Shape matches what dev-admin's `listMcpTools()` expects:
|
|
2089
|
+
{"tools": [{"name": "...", "description": "...", "schema": {...}}, ...]}
|
|
2090
|
+
"""
|
|
2091
|
+
try:
|
|
2092
|
+
from tina4_python.mcp import _get_default_server
|
|
2093
|
+
server = _get_default_server()
|
|
2094
|
+
tools = [
|
|
2095
|
+
{
|
|
2096
|
+
"name": t["name"],
|
|
2097
|
+
"description": t.get("description", ""),
|
|
2098
|
+
"schema": t.get("inputSchema") or {"type": "object", "properties": {}},
|
|
2099
|
+
}
|
|
2100
|
+
for t in server._tools.values()
|
|
2101
|
+
]
|
|
2102
|
+
return response({"tools": tools})
|
|
2103
|
+
except Exception as exc:
|
|
2104
|
+
return response({"tools": [], "error": str(exc)}, 500)
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
async def _api_mcp_call(request, response):
|
|
2108
|
+
"""POST — invoke an MCP tool by name.
|
|
2109
|
+
|
|
2110
|
+
Request: {"name": "tool_name", "arguments": {...}}
|
|
2111
|
+
Response: {"ok": true, "name": "...", "result": ...}
|
|
2112
|
+
or {"ok": false, "error": "..."}
|
|
2113
|
+
|
|
2114
|
+
The wrapper uses the tool's handler directly rather than routing
|
|
2115
|
+
through `handle_message` — we already know the name and args, no
|
|
2116
|
+
need to round-trip through JSON-RPC framing.
|
|
2117
|
+
"""
|
|
2118
|
+
body = request.body or {}
|
|
2119
|
+
if not isinstance(body, dict):
|
|
2120
|
+
return response({"ok": False, "error": "body must be a JSON object"}, 400)
|
|
2121
|
+
|
|
2122
|
+
name = body.get("name")
|
|
2123
|
+
if not name or not isinstance(name, str):
|
|
2124
|
+
return response({"ok": False, "error": "missing 'name'"}, 400)
|
|
2125
|
+
|
|
2126
|
+
args = body.get("arguments") or body.get("args") or {}
|
|
2127
|
+
if not isinstance(args, dict):
|
|
2128
|
+
return response({"ok": False, "error": "'arguments' must be an object"}, 400)
|
|
2129
|
+
|
|
2130
|
+
try:
|
|
2131
|
+
from tina4_python.mcp import _get_default_server
|
|
2132
|
+
server = _get_default_server()
|
|
2133
|
+
tool = server._tools.get(name)
|
|
2134
|
+
if tool is None:
|
|
2135
|
+
return response({"ok": False, "error": f"unknown tool: {name}"}, 404)
|
|
2136
|
+
|
|
2137
|
+
handler = tool["handler"]
|
|
2138
|
+
# Tools are registered as regular functions or coroutines;
|
|
2139
|
+
# await the result when the handler returns an awaitable.
|
|
2140
|
+
import inspect
|
|
2141
|
+
if inspect.iscoroutinefunction(handler):
|
|
2142
|
+
result = await handler(**args)
|
|
2143
|
+
else:
|
|
2144
|
+
result = handler(**args)
|
|
2145
|
+
|
|
2146
|
+
return response({"ok": True, "name": name, "result": result})
|
|
2147
|
+
except TypeError as exc:
|
|
2148
|
+
# Bad args shape — surface the Python error cleanly rather
|
|
2149
|
+
# than returning a 500. Callers see "argument X missing" etc.
|
|
2150
|
+
return response({"ok": False, "name": name, "error": f"argument error: {exc}"}, 400)
|
|
2151
|
+
except Exception as exc:
|
|
2152
|
+
return response({"ok": False, "name": name, "error": str(exc)}, 500)
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
# ─── Scaffold REST shim ────────────────────────────────────────────
|
|
2156
|
+
#
|
|
2157
|
+
# Wraps the tina4python `generate <kind> <name>` CLI commands so the
|
|
2158
|
+
# + Route / + Model / + Migration / + Middleware buttons in dev-admin
|
|
2159
|
+
# can call them without shelling out from the browser. The handlers
|
|
2160
|
+
# import the generator functions directly rather than shelling out —
|
|
2161
|
+
# avoids spawning a subprocess per click and surfaces errors as JSON.
|
|
2162
|
+
|
|
2163
|
+
_SCAFFOLD_KINDS = [
|
|
2164
|
+
{"kind": "route", "label": "+ Route", "needs_name": True},
|
|
2165
|
+
{"kind": "model", "label": "+ Model", "needs_name": True},
|
|
2166
|
+
{"kind": "migration", "label": "+ Migration", "needs_name": True},
|
|
2167
|
+
{"kind": "middleware", "label": "+ Middleware", "needs_name": True},
|
|
2168
|
+
]
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
async def _api_scaffold_list(request, response):
|
|
2172
|
+
"""GET — list the scaffold kinds this framework knows how to emit.
|
|
2173
|
+
|
|
2174
|
+
Dev-admin renders one button per item. Each carries a `kind` the
|
|
2175
|
+
/scaffold/run endpoint expects and a human `label` for the UI.
|
|
2176
|
+
"""
|
|
2177
|
+
return response({"kinds": _SCAFFOLD_KINDS})
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
async def _api_scaffold_run(request, response):
|
|
2181
|
+
"""POST — invoke a generator.
|
|
2182
|
+
|
|
2183
|
+
Request: {"kind": "route", "name": "contact"}
|
|
2184
|
+
Response: {"ok": true, "files": ["src/routes/contact.py"]}
|
|
2185
|
+
or {"ok": false, "error": "..."}
|
|
2186
|
+
|
|
2187
|
+
Uses tina4_python.cli's module-level generator functions rather
|
|
2188
|
+
than shelling out via subprocess — faster, no path/env lookup.
|
|
2189
|
+
"""
|
|
2190
|
+
body = request.body or {}
|
|
2191
|
+
if not isinstance(body, dict):
|
|
2192
|
+
return response({"ok": False, "error": "body must be a JSON object"}, 400)
|
|
2193
|
+
|
|
2194
|
+
kind = (body.get("kind") or "").strip().lower()
|
|
2195
|
+
name = (body.get("name") or "").strip()
|
|
2196
|
+
if not kind:
|
|
2197
|
+
return response({"ok": False, "error": "missing 'kind'"}, 400)
|
|
2198
|
+
if not name and kind != "auth":
|
|
2199
|
+
return response({"ok": False, "error": "missing 'name'"}, 400)
|
|
2200
|
+
|
|
2201
|
+
# Guard against path traversal / shell-metacharacter injection in
|
|
2202
|
+
# the name — generator functions pass it into file paths.
|
|
2203
|
+
import re
|
|
2204
|
+
if not re.match(r"^[A-Za-z][A-Za-z0-9_\-]*$", name):
|
|
2205
|
+
return response({"ok": False, "error": "name must match [A-Za-z][A-Za-z0-9_-]*"}, 400)
|
|
2206
|
+
|
|
2207
|
+
generator_map = {
|
|
2208
|
+
"route": "generate_route",
|
|
2209
|
+
"model": "generate_model",
|
|
2210
|
+
"migration": "generate_migration",
|
|
2211
|
+
"middleware": "generate_middleware",
|
|
2212
|
+
}
|
|
2213
|
+
fn_name = generator_map.get(kind)
|
|
2214
|
+
if fn_name is None:
|
|
2215
|
+
return response({"ok": False, "error": f"unknown scaffold kind: {kind}"}, 400)
|
|
2216
|
+
|
|
2217
|
+
try:
|
|
2218
|
+
from tina4_python import cli as cli_module
|
|
2219
|
+
fn = getattr(cli_module, fn_name, None)
|
|
2220
|
+
if fn is None:
|
|
2221
|
+
# Fall back to shelling out — keeps the endpoint useful
|
|
2222
|
+
# even if the generator function names drift.
|
|
2223
|
+
import subprocess
|
|
2224
|
+
cp = subprocess.run(
|
|
2225
|
+
["tina4python", "generate", kind, name],
|
|
2226
|
+
capture_output=True, text=True, timeout=30,
|
|
2227
|
+
)
|
|
2228
|
+
if cp.returncode != 0:
|
|
2229
|
+
return response({"ok": False, "error": cp.stderr.strip() or cp.stdout.strip()}, 500)
|
|
2230
|
+
return response({"ok": True, "output": cp.stdout.strip()})
|
|
2231
|
+
|
|
2232
|
+
# Invoke the generator directly. The CLI functions typically
|
|
2233
|
+
# print to stdout + write files; we don't capture their
|
|
2234
|
+
# output here — the file tree will refresh and show the new
|
|
2235
|
+
# files, which is what the user actually cares about.
|
|
2236
|
+
result = fn(name) if fn.__code__.co_argcount == 1 else fn(name, None)
|
|
2237
|
+
|
|
2238
|
+
# Most generators return a path or list of paths; normalise.
|
|
2239
|
+
files: list[str] = []
|
|
2240
|
+
if isinstance(result, str):
|
|
2241
|
+
files = [result]
|
|
2242
|
+
elif isinstance(result, list):
|
|
2243
|
+
files = [str(p) for p in result]
|
|
2244
|
+
return response({"ok": True, "kind": kind, "name": name, "files": files})
|
|
2245
|
+
except Exception as exc:
|
|
2246
|
+
return response({"ok": False, "error": str(exc)}, 500)
|
|
2247
|
+
|
|
2248
|
+
|
|
2059
2249
|
__all__ = ["MessageLog", "RequestInspector", "BrokenTracker",
|
|
2060
2250
|
"get_api_handlers", "render_dev_toolbar"]
|