tina4-python 3.12.9__tar.gz → 3.12.13__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 (157) hide show
  1. {tina4_python-3.12.9 → tina4_python-3.12.13}/PKG-INFO +3 -3
  2. {tina4_python-3.12.9 → tina4_python-3.12.13}/README.md +2 -2
  3. {tina4_python-3.12.9 → tina4_python-3.12.13}/pyproject.toml +1 -1
  4. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/CLAUDE.md +6 -6
  5. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/cli/__init__.py +6 -4
  7. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/request.py +20 -1
  8. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/server.py +119 -6
  9. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/debug/error_overlay.py +36 -3
  10. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/dev_admin/__init__.py +351 -127
  11. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/dev_admin/plan.py +39 -12
  12. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/mcp/tools.py +246 -11
  13. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/migration/runner.py +192 -67
  14. tina4_python-3.12.13/tina4_python/public/__feedback/widget.js +96 -0
  15. tina4_python-3.12.13/tina4_python/public/js/tina4-dev-admin.js +1089 -0
  16. tina4_python-3.12.13/tina4_python/public/js/tina4-dev-admin.min.js +1089 -0
  17. tina4_python-3.12.9/tina4_python/public/js/tina4-dev-admin.js +0 -1413
  18. tina4_python-3.12.9/tina4_python/public/js/tina4-dev-admin.min.js +0 -1413
  19. {tina4_python-3.12.9 → tina4_python-3.12.13}/.gitignore +0 -0
  20. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/HtmlElement.py +0 -0
  21. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/Testing.py +0 -0
  22. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/ai/__init__.py +0 -0
  23. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/api/__init__.py +0 -0
  24. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/auth/__init__.py +0 -0
  25. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/cache/__init__.py +0 -0
  26. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/container/__init__.py +0 -0
  27. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/__init__.py +0 -0
  28. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/cache.py +0 -0
  29. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/constants.py +0 -0
  30. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/events.py +0 -0
  31. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/middleware.py +0 -0
  32. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/rate_limiter.py +0 -0
  33. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/response.py +0 -0
  34. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/core/router.py +0 -0
  35. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/crud/__init__.py +0 -0
  36. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/__init__.py +0 -0
  37. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/adapter.py +0 -0
  38. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/connection.py +0 -0
  39. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/firebird.py +0 -0
  40. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/mongodb.py +0 -0
  41. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/mssql.py +0 -0
  42. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/mysql.py +0 -0
  43. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/odbc.py +0 -0
  44. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/postgres.py +0 -0
  45. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/database/sqlite.py +0 -0
  46. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/debug/__init__.py +0 -0
  47. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/dev_admin/metrics.py +0 -0
  48. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/dev_admin/project_index.py +0 -0
  49. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/docs.py +0 -0
  50. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/dotenv/__init__.py +0 -0
  51. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/frond/FROND.md +0 -0
  52. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/frond/__init__.py +0 -0
  53. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/frond/engine.py +0 -0
  54. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/auth/meta.json +0 -0
  55. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  56. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/database/meta.json +0 -0
  57. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  58. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/error-overlay/meta.json +0 -0
  59. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  60. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/orm/meta.json +0 -0
  61. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  62. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  63. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/queue/meta.json +0 -0
  64. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  65. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/rest-api/meta.json +0 -0
  66. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  67. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/templates/meta.json +0 -0
  68. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  69. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  70. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/graphql/__init__.py +0 -0
  71. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/i18n/__init__.py +0 -0
  72. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/mcp/__init__.py +0 -0
  73. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/mcp/protocol.py +0 -0
  74. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/messenger/__init__.py +0 -0
  75. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/migration/__init__.py +0 -0
  76. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/orm/__init__.py +0 -0
  77. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/orm/fields.py +0 -0
  78. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/orm/model.py +0 -0
  79. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/__init__.py +0 -0
  102. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  103. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_badges.scss +0 -0
  104. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  105. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_cards.scss +0 -0
  106. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_forms.scss +0 -0
  107. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_grid.scss +0 -0
  108. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_modals.scss +0 -0
  109. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_nav.scss +0 -0
  110. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_reset.scss +0 -0
  111. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_tables.scss +0 -0
  112. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_typography.scss +0 -0
  113. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  114. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/_variables.scss +0 -0
  115. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/base.scss +0 -0
  116. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/colors.scss +0 -0
  117. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/scss/tina4css/tina4.scss +0 -0
  118. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/seeder/__init__.py +0 -0
  119. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/service/__init__.py +0 -0
  120. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/session/__init__.py +0 -0
  121. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/session_handlers/__init__.py +0 -0
  122. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  123. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/session_handlers/redis_handler.py +0 -0
  124. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/session_handlers/valkey_handler.py +0 -0
  125. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/swagger/__init__.py +0 -0
  126. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/components/crud.twig +0 -0
  127. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  128. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  129. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/docker/python/Dockerfile +0 -0
  130. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  131. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/302.twig +0 -0
  132. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/401.twig +0 -0
  133. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/403.twig +0 -0
  134. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/404.twig +0 -0
  135. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/500.twig +0 -0
  136. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/502.twig +0 -0
  137. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/503.twig +0 -0
  138. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/errors/base.twig +0 -0
  139. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/frontend/README.md +0 -0
  140. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/templates/readme.md +0 -0
  141. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.12.9 → tina4_python-3.12.13}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.12.9 → tina4_python-3.12.13}/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.12.9
3
+ Version: 3.12.13
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -208,7 +208,7 @@ Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no import
208
208
  Edit `.env`:
209
209
 
210
210
  ```bash
211
- DATABASE_URL=sqlite:///data/app.db
211
+ TINA4_DATABASE_URL=sqlite:///data/app.db
212
212
  ```
213
213
 
214
214
  Create and run a migration:
@@ -695,7 +695,7 @@ Frond.clear_cache()
695
695
 
696
696
  ```bash
697
697
  SECRET=your-jwt-secret
698
- DATABASE_URL=sqlite:///data/app.db
698
+ TINA4_DATABASE_URL=sqlite:///data/app.db
699
699
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
700
700
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
701
701
  TINA4_LOCALE=en # en, fr, af, zh, ja, es
@@ -176,7 +176,7 @@ Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no import
176
176
  Edit `.env`:
177
177
 
178
178
  ```bash
179
- DATABASE_URL=sqlite:///data/app.db
179
+ TINA4_DATABASE_URL=sqlite:///data/app.db
180
180
  ```
181
181
 
182
182
  Create and run a migration:
@@ -663,7 +663,7 @@ Frond.clear_cache()
663
663
 
664
664
  ```bash
665
665
  SECRET=your-jwt-secret
666
- DATABASE_URL=sqlite:///data/app.db
666
+ TINA4_DATABASE_URL=sqlite:///data/app.db
667
667
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
668
668
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
669
669
  TINA4_LOCALE=en # en, fr, af, zh, ja, es
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.12.9"
3
+ version = "3.12.13"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -287,7 +287,7 @@ Queue(topic="tasks").push({"action": "send_email"})
287
287
  - Other methods: `.to_json()`, `.to_array()`, `.to_csv()`, `.to_paginate()`
288
288
  4. **fetch_one()**: Returns a plain dict (or None), NOT a DatabaseResult
289
289
  5. **Dict access**: All query results use dict access `row["column"]` not attribute access `row.column`
290
- 6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `DATABASE_URL`.
290
+ 6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `TINA4_DATABASE_URL`.
291
291
  7. **Running the app**: `uv run python app.py <port> <name>` — port and name are CLI args handled by tina4_python
292
292
  8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
293
293
  12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
@@ -408,7 +408,7 @@ Set `TINA4_DEBUG=true` in `.env` to enable development features:
408
408
  - **CSS hot-reload** — SCSS/CSS changes refresh stylesheets without full page reload
409
409
  - **SCSS auto-compile** — `.scss` files in `src/scss/` are compiled to `src/public/css/` on save
410
410
  - **Error overlay** — Runtime errors display a rich, syntax-highlighted overlay in the browser
411
- - **Hot-patching** — Python code changes are live-patched via jurigged (no server restart)
411
+ - **Route re-discovery** — `POST /__dev/api/reload` re-runs auto-discover, so new files in `src/routes/`, `src/orm/`, or `src/app/` register without a server restart. Existing modules are NOT re-imported — for code changes inside an already-loaded file, restart the server.
412
412
 
413
413
  DevReload connects via WebSocket at `/__dev_reload`. No configuration needed.
414
414
 
@@ -741,7 +741,7 @@ db = Database("postgresql://localhost:5432/mydb", "user", "password") # Post
741
741
  db = Database("mysql://localhost:3306/mydb", "user", "password") # MySQL
742
742
  db = Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey") # Firebird
743
743
  db = Database("mssql://localhost:1433/mydb", "sa", "password") # MSSQL
744
- db = Database() # Uses DATABASE_URL env var
744
+ db = Database() # Uses TINA4_DATABASE_URL env var
745
745
  ```
746
746
 
747
747
  ### MongoDB support
@@ -1565,9 +1565,9 @@ TINA4_API_KEY=your-api-key # Static bearer token for API auth (API_KEY fa
1565
1565
  TINA4_TOKEN_LIMIT=60 # Token lifetime in minutes (default: 60)
1566
1566
 
1567
1567
  # Database
1568
- DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
1569
- DATABASE_USERNAME= # DB username (for PostgreSQL, MySQL, etc.)
1570
- DATABASE_PASSWORD= # DB password
1568
+ TINA4_DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
1569
+ TINA4_DATABASE_USERNAME= # DB username (for PostgreSQL, MySQL, etc.)
1570
+ TINA4_DATABASE_PASSWORD= # DB password
1571
1571
 
1572
1572
  # Framework
1573
1573
  TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error overlay)
@@ -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.12.9"
11
+ __version__ = "3.12.13"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -258,7 +258,7 @@ def _console(args=None):
258
258
  from tina4_python.api import Api
259
259
  from tina4_python.core.events import on, emit
260
260
 
261
- # Try to connect database from DATABASE_URL
261
+ # Try to connect database from TINA4_DATABASE_URL
262
262
  db = None
263
263
  db_url = os.environ.get("TINA4_DATABASE_URL")
264
264
  if db_url:
@@ -345,15 +345,17 @@ def _init(args):
345
345
  encoding="utf-8",
346
346
  )
347
347
 
348
- # Create .env
348
+ # Create .env — every framework env var is TINA4_-prefixed since v3.12.
349
+ # The boot guard refuses to start with bare DATABASE_URL / SECRET set,
350
+ # so a fresh project MUST get the prefixed names.
349
351
  env_file = target / ".env"
350
352
  if not env_file.exists():
351
353
  env_file.write_text(
352
354
  "# Tina4 Configuration\n"
353
355
  "TINA4_DEBUG=true\n"
354
356
  "TINA4_LOG_LEVEL=ALL\n"
355
- "DATABASE_URL=sqlite:///data/app.db\n"
356
- 'SECRET=change-me-in-production\n',
357
+ "TINA4_DATABASE_URL=sqlite:///data/app.db\n"
358
+ 'TINA4_SECRET=change-me-in-production\n',
357
359
  encoding="utf-8",
358
360
  )
359
361
 
@@ -19,7 +19,7 @@ class Request:
19
19
  """Parsed HTTP request — everything a route handler needs."""
20
20
 
21
21
  __slots__ = (
22
- "method", "path", "query_string", "params", "query", "headers",
22
+ "method", "path", "url", "query_string", "params", "query", "headers",
23
23
  "body", "raw_body", "cookies", "files", "ip",
24
24
  "content_type", "session", "_route_params",
25
25
  )
@@ -27,6 +27,7 @@ class Request:
27
27
  def __init__(self):
28
28
  self.method: str = "GET"
29
29
  self.path: str = "/"
30
+ self.url: str = "/"
30
31
  self.query_string: str = ""
31
32
  self.params: dict = {} # Query string + route params merged
32
33
  self.query: dict = {} # Query string params only (separate from route params)
@@ -56,6 +57,24 @@ class Request:
56
57
  req.content_type = req.headers.get("content-type", "")
57
58
  req.ip = _extract_ip(scope, req.headers)
58
59
 
60
+ # Reconstruct the full absolute URL — scheme://host[:port]/path[?query].
61
+ # Honours x-forwarded-proto and x-forwarded-host so apps behind a proxy
62
+ # still see the URL the client used. Matches PHP/Ruby/Node parity.
63
+ scheme = (
64
+ req.headers.get("x-forwarded-proto")
65
+ or scope.get("scheme")
66
+ or "http"
67
+ )
68
+ host = (
69
+ req.headers.get("x-forwarded-host")
70
+ or req.headers.get("host")
71
+ or "localhost"
72
+ )
73
+ url = f"{scheme}://{host}{req.path}"
74
+ if req.query_string:
75
+ url = f"{url}?{req.query_string}"
76
+ req.url = url
77
+
59
78
  # Check upload size limit
60
79
  content_length = int(req.headers.get("content-length", 0) or 0)
61
80
  if content_length > TINA4_MAX_UPLOAD_SIZE or len(body) > TINA4_MAX_UPLOAD_SIZE:
@@ -49,19 +49,45 @@ def background(callback, interval: float = 1.0):
49
49
 
50
50
 
51
51
  def _auto_discover(root_dir: str = "src"):
52
- """Auto-import all .py files in src/ to trigger route decorators."""
52
+ """Auto-import all .py files in ``root_dir`` to trigger route decorators.
53
+
54
+ Idempotent and re-runnable: skips modules already in ``sys.modules`` so
55
+ re-discovery on /__dev/api/reload is cheap. New files added after server
56
+ boot get picked up on the next reload signal.
57
+
58
+ Import failures are recorded to ``data/.broken/`` so /health surfaces them
59
+ instead of swallowing them into a console line nobody reads.
60
+ """
53
61
  root = Path(root_dir).resolve()
54
62
  if not root.is_dir():
55
63
  return
56
64
 
65
+ # Folders to skip — non-Python sub-trees inside src/.
57
66
  skip = {"public", "templates", "scss", "locales", "icons"}
67
+ # Routes folder is special-cased so the user gets a clear warning when
68
+ # it exists but contains zero discoverable Python files.
69
+ routes_dir = root / "routes"
70
+ found_route_files = 0
58
71
 
59
72
  for py_file in sorted(root.rglob("*.py")):
60
- if any(part.startswith("_") for part in py_file.parts):
73
+ # Only filter on parts INSIDE src/, not the absolute path. Previously
74
+ # `py_file.parts` included every ancestor, so a project living under
75
+ # something like /Users/me/_archive/myapp would silently skip every
76
+ # file. Compute the relative parts first and filter on those.
77
+ try:
78
+ rel_to_root = py_file.relative_to(root)
79
+ except ValueError:
80
+ continue
81
+
82
+ rel_parts = rel_to_root.parts
83
+ if any(part.startswith("_") for part in rel_parts):
61
84
  continue
62
- if any(s in py_file.parts for s in skip):
85
+ if any(s in rel_parts for s in skip):
63
86
  continue
64
87
 
88
+ if routes_dir in py_file.parents:
89
+ found_route_files += 1
90
+
65
91
  try:
66
92
  rel = py_file.relative_to(Path.cwd()).with_suffix("")
67
93
  module_name = ".".join(rel.parts)
@@ -70,6 +96,39 @@ def _auto_discover(root_dir: str = "src"):
70
96
  Log.debug(f"Loaded: {module_name}")
71
97
  except Exception as e:
72
98
  Log.error(f"Failed to load {py_file}: {e}")
99
+ _record_broken_import(py_file, e)
100
+
101
+ # User-friendly hint: routes folder has Python files but the router is
102
+ # still empty. They almost certainly forgot the @get/@post decorator.
103
+ if found_route_files > 0:
104
+ try:
105
+ from tina4_python.core.router import Router
106
+ if not Router.get_routes():
107
+ Log.warning(
108
+ f"Auto-discover found {found_route_files} .py file(s) in "
109
+ f"{routes_dir} but no routes registered. Did you forget "
110
+ f"@get / @post / @put / @delete on your handler?"
111
+ )
112
+ except Exception:
113
+ pass
114
+
115
+
116
+ def _record_broken_import(py_file: Path, error: Exception) -> None:
117
+ """Write a .broken sentinel so /health and the dev dashboard surface
118
+ auto-discover failures instead of burying them in the console."""
119
+ try:
120
+ broken_dir = Path("data/.broken")
121
+ broken_dir.mkdir(parents=True, exist_ok=True)
122
+ import json
123
+ slug = str(py_file).replace("/", "_").replace("\\", "_")
124
+ (broken_dir / f"discover_{slug}.broken").write_text(json.dumps({
125
+ "type": "auto_discover_failure",
126
+ "file": str(py_file),
127
+ "error": f"{type(error).__name__}: {error}",
128
+ }))
129
+ except Exception:
130
+ # If even the .broken write fails we already logged the original error.
131
+ pass
73
132
 
74
133
 
75
134
  def _ensure_folders():
@@ -932,10 +991,35 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
932
991
  <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev Admin</title></head>
933
992
  <body><div id="app" data-framework="python" data-color="#3b82f6"></div>
934
993
  <script src="/js/tina4-dev-admin.min.js"></script></body></html>""")
994
+ # Never cache the dev admin shell — otherwise old HTML can
995
+ # keep loading stale widget references / outdated bundle URLs
996
+ # after we ship UX changes, leaving the user staring at
997
+ # phantoms that no longer exist server-side.
998
+ response.header("cache-control", "no-store, must-revalidate")
999
+ response.header("pragma", "no-cache")
935
1000
  else:
936
1001
  handlers = get_api_handlers()
937
1002
  handler_info = handlers.get(request.path)
938
- if handler_info and request.method == handler_info[0]:
1003
+ if not handler_info:
1004
+ # Fallback: longest-prefix wildcard match. Routes registered
1005
+ # with a trailing "/*" (e.g. "/__dev/api/threads/*") catch
1006
+ # everything under that namespace — used for parameterised
1007
+ # resources like /threads/{id} that don't fit exact-match.
1008
+ best_prefix = ""
1009
+ for key, info in handlers.items():
1010
+ if key.endswith("/*"):
1011
+ prefix = key[:-1] # keep the trailing slash
1012
+ if request.path.startswith(prefix) and len(prefix) > len(best_prefix):
1013
+ best_prefix = prefix
1014
+ handler_info = info
1015
+ # Allow "*" as a method wildcard for handlers that switch on
1016
+ # request.method themselves (REST resources with GET+POST+PATCH
1017
+ # on the same path).
1018
+ method_ok = (
1019
+ handler_info is not None
1020
+ and (handler_info[0] == "*" or request.method == handler_info[0])
1021
+ )
1022
+ if method_ok:
939
1023
  try:
940
1024
  def _resp(data, code=200, content_type=None):
941
1025
  # content_type overrides the auto-detected MIME —
@@ -955,6 +1039,14 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
955
1039
  response.status(code).json(data)
956
1040
  return data
957
1041
  _resp.render = response.render
1042
+ # Expose .stream() so handlers can return an SSE/chunked
1043
+ # response — used by the dev_admin supervisor proxy to
1044
+ # forward the agent server's text/event-stream live
1045
+ # (instead of buffering the whole multi-agent run).
1046
+ _resp.stream = response.stream
1047
+ # Expose .header() so handlers can set custom headers
1048
+ # (e.g. Cache-Control on the feedback widget bundle).
1049
+ _resp.header = response.header
958
1050
  import inspect
959
1051
  _tsig = inspect.signature(handler_info[1])
960
1052
  _tpcount = len(_tsig.parameters)
@@ -1306,12 +1398,17 @@ async def handle(request: Request) -> Response:
1306
1398
  canonical = request.path.rstrip("/") or "/"
1307
1399
  return response.status(301).header("location", canonical)
1308
1400
 
1309
- # Dev admin — also catches /ai/api/chat (SPA's ollama proxy) and the
1401
+ # Dev admin — also catches /ai/api/chat (SPA's ollama proxy), the
1310
1402
  # bare /ai /vision /embed /image /rag service-health probes that
1311
- # drive the "SERVICES ●●●●●" dots in the dev-admin UI.
1403
+ # drive the "SERVICES ●●●●●" dots in the dev-admin UI, and the
1404
+ # /__feedback/* routes used by the customer feedback widget. The
1405
+ # feedback paths live OUTSIDE /__dev because the widget is for
1406
+ # whitelisted END USERS of the shipped app — they shouldn't see
1407
+ # any /__dev URL in their network tab.
1312
1408
  _dev_extra_paths = {"/ai/api/chat", "/ai", "/vision", "/embed", "/image", "/rag"}
1313
1409
  if _is_dev and (
1314
1410
  request.path.startswith("/__dev")
1411
+ or request.path.startswith("/__feedback")
1315
1412
  or request.path in _dev_extra_paths
1316
1413
  ):
1317
1414
  return await _handle_dev_admin(request, response)
@@ -1443,6 +1540,22 @@ async def app(scope: dict, receive, send):
1443
1540
  await send({"type": "http.response.body", "body": b"", "more_body": False})
1444
1541
  return
1445
1542
 
1543
+ # Customer feedback widget injection — adds <script src="/__feedback/widget.js">
1544
+ # to HTML responses for whitelisted users. No-op if the feature is
1545
+ # off (TINA4_FEEDBACK_WHITELIST empty) or the user isn't whitelisted
1546
+ # or the body isn't HTML. Done BEFORE ETag/header build so the
1547
+ # injected bytes are included in the ETag hash + Content-Length.
1548
+ try:
1549
+ if (
1550
+ response.content
1551
+ and isinstance(response.content, (bytes, bytearray))
1552
+ and "text/html" in (response.content_type or "").lower()
1553
+ ):
1554
+ from tina4_python.dev_admin import inject_feedback_widget
1555
+ response.content = inject_feedback_widget(request, bytes(response.content))
1556
+ except Exception:
1557
+ pass # Injection is best-effort — never break the response.
1558
+
1446
1559
  # ETag check — 304 Not Modified
1447
1560
  if_none_match = request.headers.get("if-none-match", "")
1448
1561
  accept_encoding = request.headers.get("accept-encoding", "")
@@ -79,9 +79,31 @@ def _format_source_block(filename: str, lineno: int) -> str:
79
79
  )
80
80
 
81
81
 
82
- def _format_frame(frame: traceback.FrameSummary) -> str:
83
- """Render one stack frame."""
82
+ def _format_frame(frame: traceback.FrameSummary, captured_at: float = 0.0) -> str:
83
+ """Render one stack frame.
84
+
85
+ When the file was modified AFTER `captured_at`, append a
86
+ "(file modified since)" badge so a stale browser-cached overlay
87
+ can't lie about what the source looks like now. The AI coder
88
+ often rewrites files in place between page loads, leaving the
89
+ overlay's source view showing different code than what raised
90
+ the error.
91
+ """
84
92
  source = _format_source_block(frame.filename, frame.lineno) if frame.filename and frame.lineno else ""
93
+ stale_badge = ""
94
+ if captured_at and frame.filename:
95
+ try:
96
+ mtime = os.path.getmtime(frame.filename)
97
+ if mtime > captured_at + 0.5: # 0.5s margin for fs noise
98
+ from datetime import datetime, timezone
99
+ mtime_iso = datetime.fromtimestamp(mtime, tz=timezone.utc).strftime("%H:%M:%S")
100
+ stale_badge = (
101
+ f' <span style="background:{_PEACH};color:{_BG};padding:1px 8px;'
102
+ f'border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;">'
103
+ f'FILE MODIFIED @ {mtime_iso} — source may not match what failed</span>'
104
+ )
105
+ except OSError:
106
+ pass
85
107
  return (
86
108
  f'<div style="margin-bottom:16px;">'
87
109
  f'<div style="margin-bottom:4px;">'
@@ -90,6 +112,7 @@ def _format_frame(frame: traceback.FrameSummary) -> str:
90
112
  f'<span style="color:{_YELLOW};">{frame.lineno}</span>'
91
113
  f'<span style="color:{_SUBTEXT};"> in </span>'
92
114
  f'<span style="color:{_GREEN};">{_escape(frame.name)}</span>'
115
+ f"{stale_badge}"
93
116
  f"</div>"
94
117
  f"{source}"
95
118
  f"</div>"
@@ -131,14 +154,23 @@ def render_error_overlay(exception: BaseException, request: Any = None) -> str:
131
154
  Returns:
132
155
  A complete HTML page string.
133
156
  """
157
+ import time as _time
158
+ captured_at = _time.time()
159
+ captured_iso = _time.strftime("%H:%M:%S UTC", _time.gmtime(captured_at))
160
+
134
161
  exc_type = type(exception).__qualname__
135
162
  exc_msg = str(exception)
136
163
  tb = traceback.extract_tb(exception.__traceback__)
137
164
 
138
165
  # ── Stack trace ──
166
+ # Each frame compares its source file's mtime to captured_at and
167
+ # flags itself if the file has been modified since — protects
168
+ # against the "browser cached an old overlay, then the AI rewrote
169
+ # the file" confusion where displayed source no longer matches
170
+ # what actually raised the error.
139
171
  frames_html = ""
140
172
  for frame in reversed(tb):
141
- frames_html += _format_frame(frame)
173
+ frames_html += _format_frame(frame, captured_at=captured_at)
142
174
 
143
175
  # ── Request info ──
144
176
  request_pairs: list[tuple[str, str]] = []
@@ -192,6 +224,7 @@ body{{background:{_BG};color:{_TEXT};font-family:-apple-system,BlinkMacSystemFon
192
224
  <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
193
225
  <span style="background:{_RED};color:{_BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
194
226
  <span style="color:{_SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
227
+ <span style="color:{_SUBTEXT};font-size:12px;margin-left:auto;font-family:'SF Mono',Menlo,monospace;">captured {captured_iso}</span>
195
228
  </div>
196
229
  <h1 style="color:{_RED};font-size:28px;font-weight:700;margin-bottom:8px;">{_escape(exc_type)}</h1>
197
230
  <p style="color:{_TEXT};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:{_SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid {_RED};">{_escape(exc_msg)}</p>