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.
Files changed (155) hide show
  1. {tina4_python-3.11.14 → tina4_python-3.11.17}/PKG-INFO +1 -1
  2. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/__init__.py +1 -1
  3. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/server.py +32 -0
  4. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/debug/__init__.py +6 -3
  5. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dev_admin/__init__.py +190 -0
  6. tina4_python-3.11.17/tina4_python/dev_admin/plan.py +454 -0
  7. tina4_python-3.11.17/tina4_python/dev_admin/project_index.py +417 -0
  8. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/mcp/__init__.py +19 -0
  9. tina4_python-3.11.17/tina4_python/mcp/tools.py +742 -0
  10. tina4_python-3.11.17/tina4_python/public/js/tina4-dev-admin.js +1279 -0
  11. tina4_python-3.11.17/tina4_python/public/js/tina4-dev-admin.min.js +1279 -0
  12. tina4_python-3.11.14/tina4_python/mcp/tools.py +0 -348
  13. tina4_python-3.11.14/tina4_python/public/js/tina4-dev-admin.js +0 -565
  14. tina4_python-3.11.14/tina4_python/public/js/tina4-dev-admin.min.js +0 -565
  15. {tina4_python-3.11.14 → tina4_python-3.11.17}/.gitignore +0 -0
  16. {tina4_python-3.11.14 → tina4_python-3.11.17}/README.md +0 -0
  17. {tina4_python-3.11.14 → tina4_python-3.11.17}/pyproject.toml +0 -0
  18. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/CLAUDE.md +0 -0
  19. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/HtmlElement.py +0 -0
  20. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/Testing.py +0 -0
  21. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/ai/__init__.py +0 -0
  22. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/api/__init__.py +0 -0
  23. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/auth/__init__.py +0 -0
  24. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/cache/__init__.py +0 -0
  25. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/cli/__init__.py +0 -0
  26. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/container/__init__.py +0 -0
  27. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/__init__.py +0 -0
  28. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/cache.py +0 -0
  29. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/constants.py +0 -0
  30. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/events.py +0 -0
  31. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/middleware.py +0 -0
  32. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/rate_limiter.py +0 -0
  33. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/request.py +0 -0
  34. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/response.py +0 -0
  35. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/core/router.py +0 -0
  36. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/crud/__init__.py +0 -0
  37. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/__init__.py +0 -0
  38. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/adapter.py +0 -0
  39. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/connection.py +0 -0
  40. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/firebird.py +0 -0
  41. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mongodb.py +0 -0
  42. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mssql.py +0 -0
  43. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/mysql.py +0 -0
  44. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/odbc.py +0 -0
  45. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/postgres.py +0 -0
  46. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/database/sqlite.py +0 -0
  47. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/debug/error_overlay.py +0 -0
  48. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dev_admin/metrics.py +0 -0
  49. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/dotenv/__init__.py +0 -0
  50. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/FROND.md +0 -0
  51. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/__init__.py +0 -0
  52. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/frond/engine.py +0 -0
  53. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/auth/meta.json +0 -0
  54. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  55. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/database/meta.json +0 -0
  56. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  57. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/error-overlay/meta.json +0 -0
  58. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  59. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/meta.json +0 -0
  60. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  61. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  62. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/queue/meta.json +0 -0
  63. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  64. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/rest-api/meta.json +0 -0
  65. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  66. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/meta.json +0 -0
  67. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  68. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  69. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/graphql/__init__.py +0 -0
  70. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/i18n/__init__.py +0 -0
  71. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/mcp/protocol.py +0 -0
  72. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/messenger/__init__.py +0 -0
  73. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/migration/__init__.py +0 -0
  74. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/migration/runner.py +0 -0
  75. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/css/tina4.css +0 -0
  79. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/css/tina4.min.css +0 -0
  80. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/favicon.ico +0 -0
  81. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/images/logo.svg +0 -0
  82. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  83. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/tina4.min.js +0 -0
  85. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/js/tina4js.min.js +0 -0
  86. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/swagger/index.html +0 -0
  87. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  88. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/query_builder/__init__.py +0 -0
  89. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/__init__.py +0 -0
  90. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/job.py +0 -0
  91. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/kafka_backend.py +0 -0
  92. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/lite_backend.py +0 -0
  93. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/mongo_backend.py +0 -0
  94. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue/rabbitmq_backend.py +0 -0
  95. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/__init__.py +0 -0
  96. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/kafka_backend.py +0 -0
  97. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/mongo_backend.py +0 -0
  98. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/__init__.py +0 -0
  100. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  101. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_badges.scss +0 -0
  102. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  103. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_cards.scss +0 -0
  104. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_forms.scss +0 -0
  105. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_grid.scss +0 -0
  106. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_modals.scss +0 -0
  107. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_nav.scss +0 -0
  108. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_reset.scss +0 -0
  109. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_tables.scss +0 -0
  110. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_typography.scss +0 -0
  111. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  112. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/_variables.scss +0 -0
  113. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/base.scss +0 -0
  114. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/colors.scss +0 -0
  115. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/scss/tina4css/tina4.scss +0 -0
  116. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/seeder/__init__.py +0 -0
  117. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/service/__init__.py +0 -0
  118. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session/__init__.py +0 -0
  119. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/__init__.py +0 -0
  120. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  121. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/redis_handler.py +0 -0
  122. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/session_handlers/valkey_handler.py +0 -0
  123. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/swagger/__init__.py +0 -0
  124. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/components/crud.twig +0 -0
  125. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  126. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  127. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/python/Dockerfile +0 -0
  128. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  129. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/302.twig +0 -0
  130. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/401.twig +0 -0
  131. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/403.twig +0 -0
  132. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/404.twig +0 -0
  133. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/500.twig +0 -0
  134. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/502.twig +0 -0
  135. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/503.twig +0 -0
  136. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/errors/base.twig +0 -0
  137. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/frontend/README.md +0 -0
  138. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/templates/readme.md +0 -0
  139. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/test_client/__init__.py +0 -0
  140. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  141. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  142. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/validator/__init__.py +0 -0
  153. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/websocket/__init__.py +0 -0
  154. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/websocket/backplane.py +0 -0
  155. {tina4_python-3.11.14 → tina4_python-3.11.17}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.11.14
3
+ Version: 3.11.17
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.11.14"
11
+ __version__ = "3.11.17"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -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
- # Write errors to separate file
208
- if level == "error" and cls._error_writer:
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"]