tina4-python 3.11.12__tar.gz → 3.11.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 (150) hide show
  1. {tina4_python-3.11.12 → tina4_python-3.11.13}/PKG-INFO +26 -23
  2. {tina4_python-3.11.12 → tina4_python-3.11.13}/README.md +25 -22
  3. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/CLAUDE.md +4 -3
  4. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/ai/__init__.py +6 -3
  6. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/router.py +46 -14
  7. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/server.py +6 -4
  8. {tina4_python-3.11.12 → tina4_python-3.11.13}/.gitignore +0 -0
  9. {tina4_python-3.11.12 → tina4_python-3.11.13}/pyproject.toml +0 -0
  10. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/api/__init__.py +0 -0
  13. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/auth/__init__.py +0 -0
  14. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/cache/__init__.py +0 -0
  15. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/cli/__init__.py +0 -0
  16. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/__init__.py +0 -0
  18. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/cache.py +0 -0
  19. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/constants.py +0 -0
  20. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/events.py +0 -0
  21. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/middleware.py +0 -0
  22. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/rate_limiter.py +0 -0
  23. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/crud/__init__.py +0 -0
  26. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/__init__.py +0 -0
  27. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/adapter.py +0 -0
  28. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/connection.py +0 -0
  29. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/firebird.py +0 -0
  30. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/frond/engine.py +0 -0
  44. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/mcp/__init__.py +0 -0
  63. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/mcp/protocol.py +0 -0
  64. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/mcp/tools.py +0 -0
  65. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/messenger/__init__.py +0 -0
  66. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/migration/__init__.py +0 -0
  67. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/migration/runner.py +0 -0
  68. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/orm/__init__.py +0 -0
  69. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/orm/fields.py +0 -0
  70. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/orm/model.py +0 -0
  71. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/css/tina4.css +0 -0
  72. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/css/tina4.min.css +0 -0
  73. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/favicon.ico +0 -0
  74. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/images/logo.svg +0 -0
  75. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  76. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/js/frond.min.js +0 -0
  77. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  78. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  79. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/js/tina4.min.js +0 -0
  80. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/js/tina4js.min.js +0 -0
  81. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/swagger/index.html +0 -0
  82. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  83. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/query_builder/__init__.py +0 -0
  84. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/__init__.py +0 -0
  85. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/job.py +0 -0
  86. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/kafka_backend.py +0 -0
  87. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/lite_backend.py +0 -0
  88. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/mongo_backend.py +0 -0
  89. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue/rabbitmq_backend.py +0 -0
  90. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue_backends/__init__.py +0 -0
  91. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue_backends/kafka_backend.py +0 -0
  92. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue_backends/mongo_backend.py +0 -0
  93. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  94. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/__init__.py +0 -0
  95. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  96. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_badges.scss +0 -0
  97. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  98. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_cards.scss +0 -0
  99. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_forms.scss +0 -0
  100. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_grid.scss +0 -0
  101. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_modals.scss +0 -0
  102. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_nav.scss +0 -0
  103. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_reset.scss +0 -0
  104. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_tables.scss +0 -0
  105. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_typography.scss +0 -0
  106. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  107. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/_variables.scss +0 -0
  108. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/base.scss +0 -0
  109. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/colors.scss +0 -0
  110. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/scss/tina4css/tina4.scss +0 -0
  111. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/seeder/__init__.py +0 -0
  112. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/service/__init__.py +0 -0
  113. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/session/__init__.py +0 -0
  114. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/session_handlers/__init__.py +0 -0
  115. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  116. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/session_handlers/redis_handler.py +0 -0
  117. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/session_handlers/valkey_handler.py +0 -0
  118. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/swagger/__init__.py +0 -0
  119. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/components/crud.twig +0 -0
  120. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  121. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  122. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/docker/python/Dockerfile +0 -0
  123. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  124. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/302.twig +0 -0
  125. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/401.twig +0 -0
  126. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/403.twig +0 -0
  127. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/404.twig +0 -0
  128. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/500.twig +0 -0
  129. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/502.twig +0 -0
  130. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/503.twig +0 -0
  131. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/errors/base.twig +0 -0
  132. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/frontend/README.md +0 -0
  133. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/templates/readme.md +0 -0
  134. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/test_client/__init__.py +0 -0
  135. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/validator/__init__.py +0 -0
  148. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/websocket/__init__.py +0 -0
  149. {tina4_python-3.11.12 → tina4_python-3.11.13}/tina4_python/websocket/backplane.py +0 -0
  150. {tina4_python-3.11.12 → tina4_python-3.11.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.11.12
3
+ Version: 3.11.13
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
@@ -37,13 +37,13 @@ Description-Content-Type: text/markdown
37
37
  <h1 align="center">Tina4 Python</h1>
38
38
 
39
39
  <p align="center">
40
- 54 built-in features. Zero dependencies. One import, everything works.
40
+ 55 built-in features. Zero dependencies. One import, everything works.
41
41
  </p>
42
42
 
43
43
  <p align="center">
44
44
  <a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
45
- <img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
46
- <img src="https://img.shields.io/badge/features-54-blue" alt="Features">
45
+ <img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
46
+ <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
47
47
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
48
48
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
49
49
  </p>
@@ -72,10 +72,12 @@ tina4 init python ./my-app
72
72
  cd my-app && tina4 serve
73
73
  ```
74
74
 
75
- Open http://localhost:7145 — your app is running.
75
+ Open http://localhost:7146 — your app is running.
76
76
 
77
77
  <details>
78
- <summary><strong>Without the Tina4 CLI</strong></summary>
78
+ <summary><strong>Without the Tina4 CLI (Docker / CI only)</strong></summary>
79
+
80
+ The framework normally refuses to start without the `tina4` Rust CLI (it owns file watching and SCSS compilation). To bypass — e.g. inside a Docker image where you've already built the assets — set `TINA4_OVERRIDE_CLIENT=true` in `.env`:
79
81
 
80
82
  ```bash
81
83
  # 1. Create project
@@ -85,24 +87,25 @@ uv init && uv add tina4-python
85
87
  # 2. Create entry point
86
88
  echo 'from tina4_python.core import run; run()' > app.py
87
89
 
88
- # 3. Create .env
90
+ # 3. Create .env (note the override)
89
91
  echo 'TINA4_DEBUG=true' > .env
90
92
  echo 'TINA4_LOG_LEVEL=ALL' >> .env
93
+ echo 'TINA4_OVERRIDE_CLIENT=true' >> .env
91
94
 
92
95
  # 4. Create route directory
93
96
  mkdir -p src/routes
94
97
 
95
- # 5. Run
98
+ # 5. Run (no file watching, no hot reload in this mode)
96
99
  uv run python app.py
97
100
  ```
98
101
 
99
- Open http://localhost:7145
102
+ Open http://localhost:7146
100
103
 
101
104
  </details>
102
105
 
103
106
  ---
104
107
 
105
- ## What's Built In (54 Features)
108
+ ## What's Built In (55 Features)
106
109
 
107
110
  Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
108
111
 
@@ -119,7 +122,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
119
122
  | **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
120
123
  | **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
121
124
 
122
- **2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
125
+ **2,281 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
123
126
 
124
127
  For full documentation visit **[tina4.com](https://tina4.com)**.
125
128
 
@@ -198,7 +201,7 @@ async def hello_name(request, response):
198
201
  return response({"message": f"Hello, {name}!"})
199
202
  ```
200
203
 
201
- Visit `http://localhost:7145/api/hello` -- routes are auto-discovered, no imports needed.
204
+ Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no imports needed.
202
205
 
203
206
  ### 3. Add a database
204
207
 
@@ -602,10 +605,10 @@ cache.tag("users").flush()
602
605
 
603
606
  Set `TINA4_DEBUG=true` in `.env` to enable:
604
607
 
605
- - **Live reload** -- browser auto-refreshes on code changes
606
- - **CSS hot-reload** -- SCSS changes apply without page refresh
608
+ - **Live reload** -- the `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server; the framework broadcasts to the browser via WebSocket (`/__dev_reload`) with a polling fallback (`GET /__dev/api/mtime`)
609
+ - **CSS hot-reload** -- SCSS changes apply without a full page refresh
607
610
  - **Error overlay** -- rich error display in the browser
608
- - **Dev admin** at `/__dev/` with 11 tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
611
+ - **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
609
612
 
610
613
  ---
611
614
 
@@ -613,7 +616,7 @@ Set `TINA4_DEBUG=true` in `.env` to enable:
613
616
 
614
617
  ```bash
615
618
  tina4python init [dir] # Scaffold a new project
616
- tina4python serve [port] # Start dev server (default: 7145)
619
+ tina4python serve [--port P] [--no-browser] [--no-reload] # Dev server (default: 0.0.0.0:7146)
617
620
  tina4python serve --production # Auto-install and use best production server (uvicorn)
618
621
  tina4python migrate # Run pending migrations
619
622
  tina4python migrate:create <desc> # Create a migration file
@@ -715,13 +718,13 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
715
718
 
716
719
  | Framework | JSON req/s | Deps | Features |
717
720
  |-----------|-----------|------|----------|
718
- | **Tina4 Python** | **6,508** | 0 | 54 |
721
+ | **Tina4 Python** | **6,508** | 0 | 55 |
719
722
  | FastAPI | 12,652 | 12+ | ~8 |
720
723
  | Flask | 4,928 | 6+ | ~7 |
721
724
  | Bottle | 4,355 | 0 | ~5 |
722
725
  | Django | 4,050 | 20+ | ~22 |
723
726
 
724
- Tina4 Python delivers competitive throughput with **zero dependencies and 54 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
727
+ Tina4 Python delivers competitive throughput with **zero dependencies and 55 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
725
728
 
726
729
  **Across all 4 Tina4 implementations:**
727
730
 
@@ -729,7 +732,7 @@ Tina4 Python delivers competitive throughput with **zero dependencies and 54 fea
729
732
  |---|--------|-----|------|---------|
730
733
  | **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
731
734
  | **Dependencies** | 0 | 0 | 0 | 0 |
732
- | **Features** | 54 | 54 | 54 | 54 |
735
+ | **Features** | 55 | 55 | 55 | 55 |
733
736
 
734
737
  Run benchmarks locally: `python benchmarks/benchmark.py --python`
735
738
 
@@ -737,15 +740,15 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
737
740
 
738
741
  ## Cross-Framework Parity
739
742
 
740
- Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
743
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
741
744
 
742
745
  | | Python | PHP | Ruby | Node.js |
743
746
  |---|--------|-----|------|---------|
744
747
  | **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
745
- | **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
746
- | **Default port** | 7145 | 7146 | 7147 | 7148 |
748
+ | **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
749
+ | **Default port** | 7146 | 7145 | 7147 | 7148 |
747
750
 
748
- **7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
751
+ **~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
749
752
 
750
753
  ---
751
754
 
@@ -5,13 +5,13 @@
5
5
  <h1 align="center">Tina4 Python</h1>
6
6
 
7
7
  <p align="center">
8
- 54 built-in features. Zero dependencies. One import, everything works.
8
+ 55 built-in features. Zero dependencies. One import, everything works.
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
13
- <img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
14
- <img src="https://img.shields.io/badge/features-54-blue" alt="Features">
13
+ <img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
14
+ <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
15
15
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
16
16
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
17
17
  </p>
@@ -40,10 +40,12 @@ tina4 init python ./my-app
40
40
  cd my-app && tina4 serve
41
41
  ```
42
42
 
43
- Open http://localhost:7145 — your app is running.
43
+ Open http://localhost:7146 — your app is running.
44
44
 
45
45
  <details>
46
- <summary><strong>Without the Tina4 CLI</strong></summary>
46
+ <summary><strong>Without the Tina4 CLI (Docker / CI only)</strong></summary>
47
+
48
+ The framework normally refuses to start without the `tina4` Rust CLI (it owns file watching and SCSS compilation). To bypass — e.g. inside a Docker image where you've already built the assets — set `TINA4_OVERRIDE_CLIENT=true` in `.env`:
47
49
 
48
50
  ```bash
49
51
  # 1. Create project
@@ -53,24 +55,25 @@ uv init && uv add tina4-python
53
55
  # 2. Create entry point
54
56
  echo 'from tina4_python.core import run; run()' > app.py
55
57
 
56
- # 3. Create .env
58
+ # 3. Create .env (note the override)
57
59
  echo 'TINA4_DEBUG=true' > .env
58
60
  echo 'TINA4_LOG_LEVEL=ALL' >> .env
61
+ echo 'TINA4_OVERRIDE_CLIENT=true' >> .env
59
62
 
60
63
  # 4. Create route directory
61
64
  mkdir -p src/routes
62
65
 
63
- # 5. Run
66
+ # 5. Run (no file watching, no hot reload in this mode)
64
67
  uv run python app.py
65
68
  ```
66
69
 
67
- Open http://localhost:7145
70
+ Open http://localhost:7146
68
71
 
69
72
  </details>
70
73
 
71
74
  ---
72
75
 
73
- ## What's Built In (54 Features)
76
+ ## What's Built In (55 Features)
74
77
 
75
78
  Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
76
79
 
@@ -87,7 +90,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
87
90
  | **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
88
91
  | **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
89
92
 
90
- **2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
93
+ **2,281 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
91
94
 
92
95
  For full documentation visit **[tina4.com](https://tina4.com)**.
93
96
 
@@ -166,7 +169,7 @@ async def hello_name(request, response):
166
169
  return response({"message": f"Hello, {name}!"})
167
170
  ```
168
171
 
169
- Visit `http://localhost:7145/api/hello` -- routes are auto-discovered, no imports needed.
172
+ Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no imports needed.
170
173
 
171
174
  ### 3. Add a database
172
175
 
@@ -570,10 +573,10 @@ cache.tag("users").flush()
570
573
 
571
574
  Set `TINA4_DEBUG=true` in `.env` to enable:
572
575
 
573
- - **Live reload** -- browser auto-refreshes on code changes
574
- - **CSS hot-reload** -- SCSS changes apply without page refresh
576
+ - **Live reload** -- the `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server; the framework broadcasts to the browser via WebSocket (`/__dev_reload`) with a polling fallback (`GET /__dev/api/mtime`)
577
+ - **CSS hot-reload** -- SCSS changes apply without a full page refresh
575
578
  - **Error overlay** -- rich error display in the browser
576
- - **Dev admin** at `/__dev/` with 11 tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
579
+ - **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
577
580
 
578
581
  ---
579
582
 
@@ -581,7 +584,7 @@ Set `TINA4_DEBUG=true` in `.env` to enable:
581
584
 
582
585
  ```bash
583
586
  tina4python init [dir] # Scaffold a new project
584
- tina4python serve [port] # Start dev server (default: 7145)
587
+ tina4python serve [--port P] [--no-browser] [--no-reload] # Dev server (default: 0.0.0.0:7146)
585
588
  tina4python serve --production # Auto-install and use best production server (uvicorn)
586
589
  tina4python migrate # Run pending migrations
587
590
  tina4python migrate:create <desc> # Create a migration file
@@ -683,13 +686,13 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
683
686
 
684
687
  | Framework | JSON req/s | Deps | Features |
685
688
  |-----------|-----------|------|----------|
686
- | **Tina4 Python** | **6,508** | 0 | 54 |
689
+ | **Tina4 Python** | **6,508** | 0 | 55 |
687
690
  | FastAPI | 12,652 | 12+ | ~8 |
688
691
  | Flask | 4,928 | 6+ | ~7 |
689
692
  | Bottle | 4,355 | 0 | ~5 |
690
693
  | Django | 4,050 | 20+ | ~22 |
691
694
 
692
- Tina4 Python delivers competitive throughput with **zero dependencies and 54 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
695
+ Tina4 Python delivers competitive throughput with **zero dependencies and 55 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
693
696
 
694
697
  **Across all 4 Tina4 implementations:**
695
698
 
@@ -697,7 +700,7 @@ Tina4 Python delivers competitive throughput with **zero dependencies and 54 fea
697
700
  |---|--------|-----|------|---------|
698
701
  | **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
699
702
  | **Dependencies** | 0 | 0 | 0 | 0 |
700
- | **Features** | 54 | 54 | 54 | 54 |
703
+ | **Features** | 55 | 55 | 55 | 55 |
701
704
 
702
705
  Run benchmarks locally: `python benchmarks/benchmark.py --python`
703
706
 
@@ -705,15 +708,15 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
705
708
 
706
709
  ## Cross-Framework Parity
707
710
 
708
- Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
711
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
709
712
 
710
713
  | | Python | PHP | Ruby | Node.js |
711
714
  |---|--------|-----|------|---------|
712
715
  | **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
713
- | **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
714
- | **Default port** | 7145 | 7146 | 7147 | 7148 |
716
+ | **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
717
+ | **Default port** | 7146 | 7145 | 7147 | 7148 |
715
718
 
716
- **7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
719
+ **~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
717
720
 
718
721
  ---
719
722
 
@@ -632,12 +632,13 @@ Frond.add_test("positive", lambda x: x > 0)
632
632
 
633
633
  ### The @template() decorator
634
634
 
635
- Auto-renders a dict return value through a template:
635
+ Auto-renders a dict return value through a template. **Stack `@template` BELOW the route decorator** — route decorators capture the current function when applied, so `@template` above `@get` is never reached by the router:
636
+
636
637
  ```python
637
638
  from tina4_python.core.router import get, template
638
639
 
639
- @template("pages/dashboard.twig")
640
640
  @get("/dashboard")
641
+ @template("pages/dashboard.twig")
641
642
  async def dashboard(request, response):
642
643
  return {"title": "Dashboard", "stats": get_stats()}
643
644
  ```
@@ -1779,8 +1780,8 @@ for job in queue.consume("emails"):
1779
1780
  # src/routes/dashboard.py
1780
1781
  from tina4_python.core.router import get, template
1781
1782
 
1782
- @template("pages/dashboard.twig")
1783
1783
  @get("/dashboard")
1784
+ @template("pages/dashboard.twig")
1784
1785
  async def dashboard(request, response):
1785
1786
  stats = db.fetch("SELECT count(*) as total FROM orders").to_array()
1786
1787
  return {"title": "Dashboard", "stats": stats}
@@ -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.12"
11
+ __version__ = "3.11.13"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -20,6 +20,7 @@ AI_TOOLS = [
20
20
  {"name": "aider", "description": "Aider", "context_file": "CONVENTIONS.md", "config_dir": None},
21
21
  {"name": "cline", "description": "Cline", "context_file": ".clinerules", "config_dir": None},
22
22
  {"name": "codex", "description": "OpenAI Codex", "context_file": "AGENTS.md", "config_dir": None},
23
+ {"name": "antigravity", "description": "Google Antigravity", "context_file": ".antigravity/context.md", "config_dir": ".antigravity"},
23
24
  ]
24
25
 
25
26
 
@@ -40,10 +41,11 @@ def show_menu(root: str = ".") -> str:
40
41
  marker = f" {green}[installed]{reset}" if installed else ""
41
42
  print(f" {i}. {tool['description']:<20s} {tool['context_file']}{marker}")
42
43
 
43
- # tina4-ai tools option
44
+ # tina4-ai tools option — rendered AFTER the tool list, always the last number
45
+ tina4_ai_num = len(AI_TOOLS) + 1
44
46
  tina4_ai_installed = shutil.which("mdview") is not None
45
47
  marker = f" {green}[installed]{reset}" if tina4_ai_installed else ""
46
- print(f" 8. Install tina4-ai tools (requires Python){marker}")
48
+ print(f" {tina4_ai_num}. Install tina4-ai tools (requires Python){marker}")
47
49
  print()
48
50
  return input(" Select (comma-separated, or 'all'): ").strip()
49
51
 
@@ -56,6 +58,7 @@ def install_selected(root: str, selection: str) -> list[str]:
56
58
  """
57
59
  root_path = Path(root).resolve()
58
60
  created = []
61
+ tina4_ai_num = len(AI_TOOLS) + 1 # keep menu numbering aligned with show_menu
59
62
 
60
63
  if selection.lower() == "all":
61
64
  indices = list(range(len(AI_TOOLS)))
@@ -67,7 +70,7 @@ def install_selected(root: str, selection: str) -> list[str]:
67
70
  for p in parts:
68
71
  try:
69
72
  n = int(p)
70
- if n == 8:
73
+ if n == tina4_ai_num:
71
74
  install_tina4_ai = True
72
75
  elif 1 <= n <= len(AI_TOOLS):
73
76
  indices.append(n - 1)
@@ -342,19 +342,49 @@ class Router:
342
342
  _ws_routes.clear()
343
343
 
344
344
 
345
+ # Supported typed-parameter constraints. Keys are the type name written in
346
+ # the route pattern (e.g. ``{id:int}``); values are the regex that the param
347
+ # must match. Mirrored verbatim in PHP/Ruby/Node.js for cross-framework parity.
348
+ #
349
+ # Any type name that isn't in this table raises at route registration time —
350
+ # we never silently fall through to the default matcher, because a typo like
351
+ # ``{id:inetger}`` would otherwise match anything and create a security
352
+ # footgun (see tina4-book#125).
353
+ _TYPE_PATTERNS = {
354
+ "string": "[^/]+", # default, any non-slash segment
355
+ "int": r"\d+",
356
+ "integer": r"\d+",
357
+ "float": r"[\d.]+",
358
+ "number": r"[\d.]+",
359
+ "alpha": "[A-Za-z]+", # letters only
360
+ "alnum": "[A-Za-z0-9]+", # letters + digits
361
+ "slug": "[a-z0-9-]+", # URL slug
362
+ "uuid": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
363
+ "path": ".+", # greedy — matches remaining path
364
+ ".*": ".+",
365
+ }
366
+
367
+
345
368
  def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
346
369
  """Convert a route path to a regex pattern.
347
370
 
348
371
  Supports:
349
- /api/users → exact match
350
- /api/users/{id} → named parameter (any non-slash chars)
351
- /api/files/{path:path} greedy (matches remaining path)
372
+ /api/users → exact match
373
+ /api/users/{id} → named parameter (any non-slash chars)
374
+ /api/users/{id:int} digits only
375
+ /api/users/{name:alpha} → letters only
376
+ /api/users/{slug:slug} → URL slug (a-z 0-9 -)
377
+ /api/users/{id:uuid} → UUID v4 format
378
+ /api/files/{p:path} → greedy (matches remaining path)
379
+ /api/docs/* → bare-wildcard catch-all (key "*")
380
+
381
+ Unknown type names raise ``ValueError`` at route registration time.
352
382
  """
353
383
  param_names = []
354
384
  regex_parts = []
355
385
 
356
386
  segments = path.strip("/").split("/")
357
- for i, segment in enumerate(segments):
387
+ for segment in segments:
358
388
  if segment == "*":
359
389
  # Wildcard: matches the rest of the path (greedy)
360
390
  param_names.append("*")
@@ -364,14 +394,12 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
364
394
  inner = segment[1:-1]
365
395
  if ":" in inner:
366
396
  name, type_hint = inner.split(":", 1)
367
- if type_hint == "path":
368
- regex_parts.append("(.+)")
369
- elif type_hint == "int":
370
- regex_parts.append("(\\d+)")
371
- elif type_hint == "float":
372
- regex_parts.append("([\\d.]+)")
373
- else:
374
- regex_parts.append("([^/]+)")
397
+ if type_hint not in _TYPE_PATTERNS:
398
+ raise ValueError(
399
+ f"Unknown param type {type_hint!r} in route {path!r}. "
400
+ f"Valid types: {', '.join(sorted(k for k in _TYPE_PATTERNS if k != '.*'))}."
401
+ )
402
+ regex_parts.append("(" + _TYPE_PATTERNS[type_hint] + ")")
375
403
  else:
376
404
  name = inner
377
405
  regex_parts.append("([^/]+)")
@@ -518,9 +546,13 @@ def cached(max_age: int = 60):
518
546
  def template(template_name: str):
519
547
  """Auto-render a dict return value through a Frond/Twig template.
520
548
 
521
- Usage:
522
- @template("pages/dashboard.twig")
549
+ IMPORTANT: ``@template`` must sit BELOW the route decorator so the
550
+ wrapper is registered (route decorators capture the current function
551
+ reference when applied — a @template above @get never reaches the
552
+ router). Correct order:
553
+
523
554
  @get("/dashboard")
555
+ @template("pages/dashboard.twig")
524
556
  async def dashboard(request, response):
525
557
  return {"title": "Dashboard", "items": get_items()}
526
558
 
@@ -253,7 +253,7 @@ def _is_gallery_deployed(name: str) -> bool:
253
253
  def _gallery_btn(name: str, try_url: str) -> str:
254
254
  """Render a Try It or View button depending on deployment state."""
255
255
  if _is_gallery_deployed(name):
256
- return f'<button class="try-btn" style="background:#22c55e;" onclick="window.location.href=\'{try_url}\'" data-deployed="1">View &#8599;</button>'
256
+ return f'<button class="try-btn" style="background:#22c55e;" onclick="window.open(\'{try_url}\',\'_blank\')" data-deployed="1">View &#8599;</button>'
257
257
  return f'<button class="try-btn" onclick="deployGallery(\'{name}\',\'{try_url}\')">Try It</button>'
258
258
 
259
259
 
@@ -437,18 +437,20 @@ function deployGallery(name, tryUrl) {{
437
437
  btn.style.background = '#22c55e';
438
438
  btn.disabled = false;
439
439
  btn.dataset.deployed = '1';
440
- // Wait for the newly deployed route to become reachable before navigating
440
+ // Wait for the newly deployed route to become reachable, then
441
+ // open it in a new tab so the dev-admin / gallery home stays
442
+ // open (fixes tina4-book#115).
441
443
  var attempts = 0;
442
444
  var maxAttempts = 5;
443
445
  function pollRoute() {{
444
446
  fetch(tryUrl, {{method: 'HEAD'}}).then(function() {{
445
- window.location.href = tryUrl;
447
+ window.open(tryUrl, '_blank');
446
448
  }}).catch(function() {{
447
449
  attempts++;
448
450
  if (attempts < maxAttempts) {{
449
451
  setTimeout(pollRoute, 500);
450
452
  }} else {{
451
- window.location.href = tryUrl;
453
+ window.open(tryUrl, '_blank');
452
454
  }}
453
455
  }});
454
456
  }}