tina4-python 3.10.58__tar.gz → 3.10.60__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 (144) hide show
  1. {tina4_python-3.10.58 → tina4_python-3.10.60}/PKG-INFO +1 -1
  2. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/HtmlElement.py +1 -1
  3. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/Testing.py +1 -1
  4. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/__init__.py +2 -2
  5. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/cli/__init__.py +52 -0
  6. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/router.py +6 -6
  7. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/server.py +79 -17
  8. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/dev_admin/metrics.py +21 -2
  9. {tina4_python-3.10.58 → tina4_python-3.10.60}/.gitignore +0 -0
  10. {tina4_python-3.10.58 → tina4_python-3.10.60}/README.md +0 -0
  11. {tina4_python-3.10.58 → tina4_python-3.10.60}/pyproject.toml +0 -0
  12. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/CLAUDE.md +0 -0
  13. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/ai/__init__.py +0 -0
  14. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/crud/__init__.py +0 -0
  26. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/__init__.py +0 -0
  27. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/adapter.py +0 -0
  28. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/connection.py +0 -0
  29. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/firebird.py +0 -0
  30. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/dev_reload.py +0 -0
  40. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/frond/engine.py +0 -0
  44. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/mcp/__init__.py +0 -0
  63. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/mcp/protocol.py +0 -0
  64. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/mcp/tools.py +0 -0
  65. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/messenger/__init__.py +0 -0
  66. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/migration/__init__.py +0 -0
  67. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/migration/runner.py +0 -0
  68. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/orm/__init__.py +0 -0
  69. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/orm/fields.py +0 -0
  70. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/orm/model.py +0 -0
  71. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/css/tina4.css +0 -0
  72. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/css/tina4.min.css +0 -0
  73. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/favicon.ico +0 -0
  74. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/images/logo.svg +0 -0
  75. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  76. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/js/frond.min.js +0 -0
  77. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  78. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/js/tina4.min.js +0 -0
  79. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/js/tina4js.min.js +0 -0
  80. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/swagger/index.html +0 -0
  81. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  82. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/query_builder/__init__.py +0 -0
  83. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/queue/__init__.py +0 -0
  84. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/queue_backends/__init__.py +0 -0
  85. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/queue_backends/kafka_backend.py +0 -0
  86. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/queue_backends/mongo_backend.py +0 -0
  87. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  88. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/__init__.py +0 -0
  89. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  90. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_badges.scss +0 -0
  91. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  92. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_cards.scss +0 -0
  93. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_forms.scss +0 -0
  94. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_grid.scss +0 -0
  95. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_modals.scss +0 -0
  96. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_nav.scss +0 -0
  97. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_reset.scss +0 -0
  98. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_tables.scss +0 -0
  99. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_typography.scss +0 -0
  100. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  101. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/_variables.scss +0 -0
  102. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/base.scss +0 -0
  103. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/colors.scss +0 -0
  104. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/scss/tina4css/tina4.scss +0 -0
  105. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/seeder/__init__.py +0 -0
  106. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/service/__init__.py +0 -0
  107. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/session/__init__.py +0 -0
  108. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/session_handlers/__init__.py +0 -0
  109. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  110. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/session_handlers/redis_handler.py +0 -0
  111. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/session_handlers/valkey_handler.py +0 -0
  112. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/swagger/__init__.py +0 -0
  113. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/components/crud.twig +0 -0
  114. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  115. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  116. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/docker/python/Dockerfile +0 -0
  117. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  118. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/302.twig +0 -0
  119. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/401.twig +0 -0
  120. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/403.twig +0 -0
  121. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/404.twig +0 -0
  122. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/500.twig +0 -0
  123. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/502.twig +0 -0
  124. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/503.twig +0 -0
  125. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/errors/base.twig +0 -0
  126. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/frontend/README.md +0 -0
  127. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/templates/readme.md +0 -0
  128. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/test_client/__init__.py +0 -0
  129. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  130. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  131. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  132. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  133. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  134. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  135. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/validator/__init__.py +0 -0
  142. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/websocket/__init__.py +0 -0
  143. {tina4_python-3.10.58 → tina4_python-3.10.60}/tina4_python/websocket/backplane.py +0 -0
  144. {tina4_python-3.10.58 → tina4_python-3.10.60}/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.10.58
3
+ Version: 3.10.60
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
@@ -1,5 +1,5 @@
1
1
  """
2
- Tina4 - This is not a 4ramework.
2
+ Tina4 - The Intelligent Native Application 4ramework.
3
3
  Programmatic HTML builder - avoids string concatenation.
4
4
 
5
5
  Usage:
@@ -1,4 +1,4 @@
1
- # Tina4 Python v3.0 — This is not a 4ramework.
1
+ # Tina4 Python v3.0 — The Intelligent Native Application 4ramework.
2
2
  # Copyright 2007 - present Tina4
3
3
  # License: MIT https://opensource.org/licenses/MIT
4
4
  """
@@ -1,4 +1,4 @@
1
- # Tina4 Python v3.0 — This is not a 4ramework.
1
+ # Tina4 Python v3.0 — The Intelligent Native Application 4ramework.
2
2
  # Copyright 2007 - present Tina4
3
3
  # License: MIT https://opensource.org/licenses/MIT
4
4
  """
@@ -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.10.58"
11
+ __version__ = "3.10.60"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -137,6 +137,7 @@ def main():
137
137
  "build": _build,
138
138
  "ai": _ai,
139
139
  "generate": _generate,
140
+ "console": _console,
140
141
  "help": _help,
141
142
  }
142
143
 
@@ -166,6 +167,7 @@ Commands:
166
167
  test Run test suite
167
168
  build Build distributable package
168
169
  ai [--all] Install AI coding assistant context
170
+ console Start interactive REPL with framework loaded
169
171
 
170
172
  Generators:
171
173
  generate model <Name> [--fields "name:string,price:float"]
@@ -185,6 +187,56 @@ https://tina4.com
185
187
  """)
186
188
 
187
189
 
190
+ # ── Console ───────────────────────────────────────────────────────────
191
+
192
+ def _console(args=None):
193
+ """Start an interactive REPL with the framework loaded."""
194
+ import code
195
+ import os
196
+
197
+ # Load environment
198
+ from tina4_python.dotenv import load_env
199
+ load_env()
200
+
201
+ # Import everything the user needs
202
+ from tina4_python import get, post, put, patch, delete, Router, Database, ORM, Auth, Queue, Frond
203
+ from tina4_python.debug import Log
204
+ from tina4_python.api import Api
205
+ from tina4_python.core.events import on, emit
206
+
207
+ # Try to connect database from DATABASE_URL
208
+ db = None
209
+ db_url = os.environ.get("DATABASE_URL")
210
+ if db_url:
211
+ try:
212
+ db = Database(db_url)
213
+ print(f" Database: {db_url}")
214
+ except Exception as e:
215
+ print(f" Database: failed ({e})")
216
+
217
+ # Auto-discover routes
218
+ from tina4_python.core.server import _auto_discover
219
+ _auto_discover("src")
220
+ route_count = len(Router.get_routes())
221
+ print(f" Routes: {route_count} discovered")
222
+
223
+ banner = (
224
+ "\n Tina4 Python Console\n"
225
+ " Type Python code. Framework is loaded.\n"
226
+ " Available: db, Router, ORM, Database, Auth, Api, Log, Queue\n"
227
+ " Exit: Ctrl+D or exit()\n"
228
+ )
229
+
230
+ local_vars = {
231
+ "db": db, "Database": Database, "ORM": ORM, "Router": Router,
232
+ "Auth": Auth, "Api": Api, "Log": Log, "Queue": Queue,
233
+ "Frond": Frond, "get": get, "post": post, "put": put,
234
+ "patch": patch, "delete": delete, "on": on, "emit": emit,
235
+ }
236
+
237
+ code.interact(banner=banner, local=local_vars)
238
+
239
+
188
240
  # ── Init ──────────────────────────────────────────────────────────────
189
241
 
190
242
  def _init(args):
@@ -79,22 +79,22 @@ class RouteGroup:
79
79
  self._middleware = middleware or []
80
80
 
81
81
  def get(self, path: str, handler, **options) -> RouteRef:
82
- return self._router.add("GET", self._prefix + path, handler, middleware=self._middleware, **options)
82
+ return self._router.add("GET", path, handler, middleware=self._middleware, **options)
83
83
 
84
84
  def post(self, path: str, handler, **options) -> RouteRef:
85
- return self._router.add("POST", self._prefix + path, handler, middleware=self._middleware, **options)
85
+ return self._router.add("POST", path, handler, middleware=self._middleware, **options)
86
86
 
87
87
  def put(self, path: str, handler, **options) -> RouteRef:
88
- return self._router.add("PUT", self._prefix + path, handler, middleware=self._middleware, **options)
88
+ return self._router.add("PUT", path, handler, middleware=self._middleware, **options)
89
89
 
90
90
  def patch(self, path: str, handler, **options) -> RouteRef:
91
- return self._router.add("PATCH", self._prefix + path, handler, middleware=self._middleware, **options)
91
+ return self._router.add("PATCH", path, handler, middleware=self._middleware, **options)
92
92
 
93
93
  def delete(self, path: str, handler, **options) -> RouteRef:
94
- return self._router.add("DELETE", self._prefix + path, handler, middleware=self._middleware, **options)
94
+ return self._router.add("DELETE", path, handler, middleware=self._middleware, **options)
95
95
 
96
96
  def any(self, path: str, handler, **options) -> RouteRef:
97
- return self._router.add("ANY", self._prefix + path, handler, middleware=self._middleware, **options)
97
+ return self._router.add("ANY", path, handler, middleware=self._middleware, **options)
98
98
 
99
99
  def group(self, prefix: str, callback, middleware=None):
100
100
  merged = list(self._middleware) + (middleware or [])
@@ -254,7 +254,7 @@ h1{{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}}
254
254
  <div class="hero">
255
255
  <img src="/images/tina4-logo-icon.webp" class="logo" alt="Tina4">
256
256
  <h1>Tina4Python</h1>
257
- <p class="tagline">This Is Now A 4Framework</p>
257
+ <p class="tagline">The Intelligent Native Application 4ramework</p>
258
258
  <div class="actions">
259
259
  <a href="https://tina4.com/python" class="btn" target="_blank">Website</a>
260
260
  <a href="/__dev" class="btn">Dev Admin</a>
@@ -1175,19 +1175,84 @@ def _find_production_server():
1175
1175
  return None
1176
1176
 
1177
1177
 
1178
+ def _kill_port(port: int) -> None:
1179
+ """Kill whatever process is listening on *port*.
1180
+
1181
+ Uses lsof on macOS/Linux and netstat + taskkill on Windows.
1182
+ Raises RuntimeError if the port cannot be freed.
1183
+ """
1184
+ import subprocess
1185
+ import time
1186
+
1187
+ print(f" Port {port} in use — killing existing process...")
1188
+
1189
+ if sys.platform == "win32":
1190
+ # Find PID via netstat
1191
+ try:
1192
+ result = subprocess.run(
1193
+ ["netstat", "-ano"],
1194
+ capture_output=True, text=True, timeout=5
1195
+ )
1196
+ pid = None
1197
+ for line in result.stdout.splitlines():
1198
+ if f":{port}" in line and ("LISTENING" in line or "ESTABLISHED" in line):
1199
+ parts = line.split()
1200
+ if parts:
1201
+ pid = parts[-1]
1202
+ break
1203
+ if pid and pid.isdigit():
1204
+ subprocess.run(["taskkill", "/PID", pid, "/F"], timeout=5)
1205
+ except Exception as e:
1206
+ raise RuntimeError(f"Could not free port {port}: {e}") from e
1207
+ else:
1208
+ # macOS / Linux — use lsof
1209
+ try:
1210
+ result = subprocess.run(
1211
+ ["lsof", "-ti", f":{port}"],
1212
+ capture_output=True, text=True, timeout=5
1213
+ )
1214
+ pids = result.stdout.strip().splitlines()
1215
+ if not pids:
1216
+ return # Nothing found — port may have freed itself
1217
+ for pid_str in pids:
1218
+ pid_str = pid_str.strip()
1219
+ if pid_str.isdigit():
1220
+ os.kill(int(pid_str), signal.SIGTERM)
1221
+ except FileNotFoundError:
1222
+ # lsof not available — try fuser
1223
+ try:
1224
+ result = subprocess.run(
1225
+ ["fuser", f"{port}/tcp"],
1226
+ capture_output=True, text=True, timeout=5
1227
+ )
1228
+ for pid_str in result.stdout.split():
1229
+ if pid_str.isdigit():
1230
+ os.kill(int(pid_str), signal.SIGTERM)
1231
+ except Exception as e:
1232
+ raise RuntimeError(f"Could not free port {port}: {e}") from e
1233
+ except Exception as e:
1234
+ raise RuntimeError(f"Could not free port {port}: {e}") from e
1235
+
1236
+ # Give the OS a moment to reclaim the port
1237
+ time.sleep(0.5)
1238
+ print(f" Port {port} freed")
1239
+
1240
+
1178
1241
  def _find_available_port(start: int, max_tries: int = 10) -> int:
1179
- """Try successive ports starting from *start*, return the first available."""
1242
+ """Check if *start* is available; if not, kill the process on it and return *start*.
1243
+
1244
+ The auto-increment behaviour is intentionally removed — the server always
1245
+ claims the requested port. If killing fails a RuntimeError is raised.
1246
+ """
1180
1247
  import socket
1181
- for offset in range(max_tries):
1182
- port = start + offset
1183
- try:
1184
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1185
- s.bind(("127.0.0.1", port))
1186
- s.close()
1187
- return port
1188
- except OSError:
1189
- continue
1190
- return start
1248
+ try:
1249
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1250
+ s.bind(("127.0.0.1", start))
1251
+ s.close()
1252
+ return start
1253
+ except OSError:
1254
+ _kill_port(start)
1255
+ return start
1191
1256
 
1192
1257
 
1193
1258
  def _open_browser(url: str):
@@ -1247,7 +1312,7 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio", ai_port: i
1247
1312
  / / / / / / / /_/ /__ __/
1248
1313
  /_/ /_/_/ /_/\\__,_/ /_/
1249
1314
  {reset}
1250
- Tina4 Python v{__version__} — This Is Now A 4Framework
1315
+ Tina4 Python v{__version__} — The Intelligent Native Application 4ramework
1251
1316
 
1252
1317
  Server: http://{display}:{port} ({server_name})
1253
1318
  Swagger: http://localhost:{port}/swagger
@@ -1300,11 +1365,8 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
1300
1365
  # Resolve host/port (CLI arg > ENV > default)
1301
1366
  host, port = resolve_config(cli_host=host, cli_port=port)
1302
1367
 
1303
- # Auto-increment port if already in use
1304
- original_port = port
1368
+ # Claim the requested port kill whatever is on it if needed
1305
1369
  port = _find_available_port(port)
1306
- if port != original_port:
1307
- Log.info(f"Port {original_port} in use — using {port} instead")
1308
1370
 
1309
1371
  # Detect production server (unless TINA4_DEBUG is true)
1310
1372
  from tina4_python.dotenv import is_truthy
@@ -495,7 +495,8 @@ def _count_halstead(node: ast.AST, stats: dict):
495
495
  def _has_matching_test(rel_path: str) -> bool:
496
496
  """Check if a source file has a matching test file.
497
497
 
498
- Looks for tests/test_{module}.py or tests/test_{module_name}.py.
498
+ Looks for common test file patterns and also scans any test file
499
+ that imports the module by name.
499
500
  """
500
501
  p = Path(rel_path)
501
502
  module = p.stem # e.g. "auth" from "tina4_python/auth/__init__.py"
@@ -506,8 +507,26 @@ def _has_matching_test(rel_path: str) -> bool:
506
507
  Path("tests") / f"test_{module}.py",
507
508
  Path("tests") / f"test_{module}s.py",
508
509
  Path("test") / f"test_{module}.py",
510
+ Path("spec") / f"test_{module}.py",
511
+ Path("tests") / f"{module}_test.py",
509
512
  ]
510
- return any(tp.exists() for tp in test_patterns)
513
+ if any(tp.exists() for tp in test_patterns):
514
+ return True
515
+ # Grep-based: check if any test file imports this module
516
+ import re
517
+ import_patterns = [re.compile(rf'\bimport\s+{re.escape(module)}\b'),
518
+ re.compile(rf'\bfrom\s+{re.escape(module)}\b')]
519
+ for test_dir in (Path("tests"), Path("test"), Path("spec")):
520
+ if not test_dir.is_dir():
521
+ continue
522
+ for test_file in test_dir.glob("*.py"):
523
+ try:
524
+ content = test_file.read_text(encoding="utf-8", errors="ignore")
525
+ if any(pat.search(content) for pat in import_patterns):
526
+ return True
527
+ except OSError:
528
+ pass
529
+ return False
511
530
 
512
531
 
513
532
  def _maintainability_index(halstead_volume: float, avg_cc: float, loc: int) -> float:
File without changes