tina4-python 3.11.8__tar.gz → 3.11.9__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 (151) hide show
  1. {tina4_python-3.11.8 → tina4_python-3.11.9}/PKG-INFO +1 -1
  2. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/__init__.py +1 -1
  3. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/frond/engine.py +7 -0
  4. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/session_handlers/mongodb_handler.py +2 -2
  5. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/session_handlers/redis_handler.py +2 -2
  6. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/session_handlers/valkey_handler.py +2 -2
  7. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/websocket/__init__.py +18 -2
  8. tina4_python-3.11.8/tina4_python/dev_reload.py +0 -214
  9. {tina4_python-3.11.8 → tina4_python-3.11.9}/.gitignore +0 -0
  10. {tina4_python-3.11.8 → tina4_python-3.11.9}/README.md +0 -0
  11. {tina4_python-3.11.8 → tina4_python-3.11.9}/pyproject.toml +0 -0
  12. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/CLAUDE.md +0 -0
  13. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/HtmlElement.py +0 -0
  14. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/Testing.py +0 -0
  15. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/ai/__init__.py +0 -0
  16. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/api/__init__.py +0 -0
  17. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/auth/__init__.py +0 -0
  18. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/cache/__init__.py +0 -0
  19. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/cli/__init__.py +0 -0
  20. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/container/__init__.py +0 -0
  21. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/database/sqlite.py +0 -0
  42. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/debug/__init__.py +0 -0
  43. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/debug/error_overlay.py +0 -0
  44. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/dev_admin/__init__.py +0 -0
  45. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/dev_admin/metrics.py +0 -0
  46. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/auth/meta.json +0 -0
  50. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  51. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/database/meta.json +0 -0
  52. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  53. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/error-overlay/meta.json +0 -0
  54. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  55. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/orm/meta.json +0 -0
  56. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  57. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  58. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/queue/meta.json +0 -0
  59. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  60. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/rest-api/meta.json +0 -0
  61. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  62. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/templates/meta.json +0 -0
  63. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  64. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  65. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/graphql/__init__.py +0 -0
  66. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/i18n/__init__.py +0 -0
  67. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/mcp/__init__.py +0 -0
  68. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/mcp/protocol.py +0 -0
  69. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/mcp/tools.py +0 -0
  70. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/orm/model.py +0 -0
  76. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/js/frond.min.js +0 -0
  82. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  83. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  84. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/js/tina4.min.js +0 -0
  85. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/js/tina4js.min.js +0 -0
  86. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/swagger/index.html +0 -0
  87. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  88. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/query_builder/__init__.py +0 -0
  89. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/__init__.py +0 -0
  90. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/job.py +0 -0
  91. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/kafka_backend.py +0 -0
  92. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/lite_backend.py +0 -0
  93. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/mongo_backend.py +0 -0
  94. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue/rabbitmq_backend.py +0 -0
  95. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue_backends/__init__.py +0 -0
  96. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue_backends/kafka_backend.py +0 -0
  97. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue_backends/mongo_backend.py +0 -0
  98. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/__init__.py +0 -0
  100. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  101. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_badges.scss +0 -0
  102. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  103. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_cards.scss +0 -0
  104. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_forms.scss +0 -0
  105. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_grid.scss +0 -0
  106. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_modals.scss +0 -0
  107. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_nav.scss +0 -0
  108. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_reset.scss +0 -0
  109. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_tables.scss +0 -0
  110. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_typography.scss +0 -0
  111. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  112. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/_variables.scss +0 -0
  113. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/base.scss +0 -0
  114. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/colors.scss +0 -0
  115. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/scss/tina4css/tina4.scss +0 -0
  116. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/seeder/__init__.py +0 -0
  117. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/service/__init__.py +0 -0
  118. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/session/__init__.py +0 -0
  119. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/session_handlers/__init__.py +0 -0
  120. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/swagger/__init__.py +0 -0
  121. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/components/crud.twig +0 -0
  122. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  123. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  124. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/docker/python/Dockerfile +0 -0
  125. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  126. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/302.twig +0 -0
  127. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/401.twig +0 -0
  128. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/403.twig +0 -0
  129. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/404.twig +0 -0
  130. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/500.twig +0 -0
  131. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/502.twig +0 -0
  132. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/503.twig +0 -0
  133. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/errors/base.twig +0 -0
  134. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/frontend/README.md +0 -0
  135. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/templates/readme.md +0 -0
  136. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/test_client/__init__.py +0 -0
  137. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/validator/__init__.py +0 -0
  150. {tina4_python-3.11.8 → tina4_python-3.11.9}/tina4_python/websocket/backplane.py +0 -0
  151. {tina4_python-3.11.8 → tina4_python-3.11.9}/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.8
3
+ Version: 3.11.9
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.11.8"
11
+ __version__ = "3.11.9"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -414,6 +414,13 @@ def _resolve(expr: str, context: dict):
414
414
  return None
415
415
  elif isinstance(value, dict):
416
416
  value = value.get(part)
417
+ elif isinstance(value, (list, tuple)) and part.isdigit():
418
+ # Numeric dot-index into a list/tuple: items.0.name
419
+ idx = int(part)
420
+ try:
421
+ value = value[idx]
422
+ except IndexError:
423
+ return None
417
424
  elif hasattr(value, part):
418
425
  attr = getattr(value, part)
419
426
  value = attr() if callable(attr) else attr
@@ -7,7 +7,7 @@ Environment variables:
7
7
  TINA4_SESSION_MONGO_URL — MongoDB URL (default: mongodb://localhost:27017)
8
8
  TINA4_SESSION_MONGO_DB — database name (default: tina4)
9
9
  TINA4_SESSION_MONGO_COLLECTION — collection name (default: sessions)
10
- TINA4_SESSION_TTL — session TTL in seconds (default: 1800)
10
+ TINA4_SESSION_TTL — session TTL in seconds (default: 3600)
11
11
  """
12
12
  import json
13
13
  import os
@@ -28,7 +28,7 @@ class MongoDBSessionHandler(SessionHandler):
28
28
  mongo_url = config.get("url", os.environ.get("TINA4_SESSION_MONGO_URL", "mongodb://localhost:27017"))
29
29
  self._database = config.get("database", os.environ.get("TINA4_SESSION_MONGO_DB", "tina4"))
30
30
  self._collection_name = config.get("collection", os.environ.get("TINA4_SESSION_MONGO_COLLECTION", "sessions"))
31
- self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "1800")))
31
+ self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "3600")))
32
32
 
33
33
  self._pymongo_client = None
34
34
  self._collection = None
@@ -8,7 +8,7 @@ Environment variables:
8
8
  TINA4_SESSION_REDIS_PORT — port (default: 6379)
9
9
  TINA4_SESSION_REDIS_PASSWORD — password (default: none)
10
10
  TINA4_SESSION_REDIS_DB — database number (default: 0)
11
- TINA4_SESSION_TTL — session TTL in seconds (default: 1800)
11
+ TINA4_SESSION_TTL — session TTL in seconds (default: 3600)
12
12
  """
13
13
  import json
14
14
  import os
@@ -28,7 +28,7 @@ class RedisSessionHandler(SessionHandler):
28
28
  self._port = int(config.get("port", os.environ.get("TINA4_SESSION_REDIS_PORT", "6379")))
29
29
  self._password = config.get("password") or os.environ.get("TINA4_SESSION_REDIS_PASSWORD") or None
30
30
  self._db = int(config.get("db", os.environ.get("TINA4_SESSION_REDIS_DB", "0")))
31
- self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "1800")))
31
+ self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "3600")))
32
32
  self._prefix = config.get("prefix", "tina4:session:")
33
33
 
34
34
  self._redis_client = None
@@ -9,7 +9,7 @@ Environment variables:
9
9
  TINA4_SESSION_VALKEY_PORT — port (default: 6379)
10
10
  TINA4_SESSION_VALKEY_PASSWORD — password (default: none)
11
11
  TINA4_SESSION_VALKEY_DB — database number (default: 0)
12
- TINA4_SESSION_TTL — session TTL in seconds (default: 1800)
12
+ TINA4_SESSION_TTL — session TTL in seconds (default: 3600)
13
13
  """
14
14
  import json
15
15
  import os
@@ -30,7 +30,7 @@ class ValkeySessionHandler(SessionHandler):
30
30
  self._port = int(config.get("port", os.environ.get("TINA4_SESSION_VALKEY_PORT", "6379")))
31
31
  self._password = config.get("password") or os.environ.get("TINA4_SESSION_VALKEY_PASSWORD") or None
32
32
  self._db = int(config.get("db", os.environ.get("TINA4_SESSION_VALKEY_DB", "0")))
33
- self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "1800")))
33
+ self._ttl = int(config.get("ttl", os.environ.get("TINA4_SESSION_TTL", "3600")))
34
34
  self._prefix = config.get("prefix", "tina4:session:")
35
35
 
36
36
  self._redis_client = None
@@ -418,6 +418,15 @@ class WebSocketManager:
418
418
  ids = self._rooms.get(room_name, set())
419
419
  return [self._connections[i] for i in ids if i in self._connections]
420
420
 
421
+ def get_client_rooms(self, client_id: str) -> list[str]:
422
+ """Return the list of room names a specific client belongs to.
423
+
424
+ Mirrors PHP's ``getClientRooms()`` and the per-connection ``conn.rooms``
425
+ property — useful when you have a client ID but not the connection
426
+ object itself.
427
+ """
428
+ return [room for room, members in self._rooms.items() if client_id in members]
429
+
421
430
  async def broadcast_to_room(self, room_name: str, message: str | bytes,
422
431
  exclude: str = None) -> None:
423
432
  """Send message to all connections in a room."""
@@ -437,8 +446,12 @@ class WebSocketServer:
437
446
  self._handlers: dict[str, dict[str, Callable]] = {}
438
447
  self._server: asyncio.AbstractServer | None = None
439
448
 
440
- def route(self, path: str):
441
- """Decorator to register a WebSocket handler for a path.
449
+ def route(self, path: str, handler: Callable | None = None):
450
+ """Register a WebSocket handler for a path.
451
+
452
+ Can be used either as a decorator (``@server.route("/chat")``) or
453
+ called directly with a handler (``server.route("/chat", chat_handler)``)
454
+ for parity with PHP/Ruby/Node.
442
455
 
443
456
  Registers both on this server instance (standalone mode) and on the
444
457
  main Router (integrated mode) so routes work either way.
@@ -471,6 +484,9 @@ class WebSocketServer:
471
484
  from tina4_python.core.router import Router
472
485
  Router.websocket(path, _router_adapter)
473
486
  return func
487
+
488
+ if handler is not None:
489
+ return decorator(handler)
474
490
  return decorator
475
491
 
476
492
  def on_connect(self, path: str):
@@ -1,214 +0,0 @@
1
- # Tina4 DevReload — File-change detection via mtime polling.
2
- """
3
- Watches source files for changes and triggers route re-discovery.
4
- Active only when TINA4_DEBUG=true.
5
-
6
- The browser-side polling is handled by JS injected into the dev toolbar,
7
- which polls /__dev/api/mtime and reloads when the timestamp changes.
8
-
9
- Uses simple mtime polling (no external dependencies).
10
- """
11
- import os
12
- import sys
13
- import time
14
- import importlib
15
- import threading
16
- from pathlib import Path
17
-
18
- from tina4_python.debug import Log
19
-
20
-
21
- # Watched file extensions
22
- _WATCH_EXTENSIONS = {".py", ".twig", ".html", ".css", ".scss", ".js"}
23
-
24
- # Directories to ignore (anywhere in the path)
25
- _IGNORE_DIRS = {".git", "node_modules", "vendor", "__pycache__", "data", ".venv", ".mypy_cache", ".ruff_cache"}
26
-
27
- # Module-level state
28
- _last_mtime: float = 0.0
29
- _last_change_file: str = ""
30
- _lock = threading.Lock()
31
- _running = False
32
-
33
-
34
- def get_last_mtime() -> float:
35
- """Return the most recent file modification timestamp."""
36
- return _last_mtime
37
-
38
-
39
- def get_last_change_file() -> str:
40
- """Return the path of the most recently changed file."""
41
- return _last_change_file
42
-
43
-
44
- def _should_ignore(path: Path) -> bool:
45
- """Check if a path should be ignored based on directory names."""
46
- for part in path.parts:
47
- if part in _IGNORE_DIRS:
48
- return True
49
- return False
50
-
51
-
52
- def _scan_mtime(directories: list[str]) -> tuple[float, str]:
53
- """Scan directories for the maximum file mtime.
54
-
55
- Returns (max_mtime, file_path) tuple.
56
- """
57
- max_mtime = 0.0
58
- max_file = ""
59
-
60
- for dir_path in directories:
61
- root = Path(dir_path)
62
- if not root.is_dir():
63
- continue
64
-
65
- for file_path in root.rglob("*"):
66
- if not file_path.is_file():
67
- continue
68
- if file_path.suffix not in _WATCH_EXTENSIONS:
69
- continue
70
- if _should_ignore(file_path):
71
- continue
72
-
73
- try:
74
- mtime = file_path.stat().st_mtime
75
- if mtime > max_mtime:
76
- max_mtime = mtime
77
- max_file = str(file_path)
78
- except OSError:
79
- continue
80
-
81
- return max_mtime, max_file
82
-
83
-
84
- def _rediscover_routes():
85
- """Re-import changed Python modules in src/ to pick up new/changed routes.
86
-
87
- Clears the route registry and re-discovers all routes from scratch.
88
- This ensures removed routes are also cleaned up.
89
- """
90
- from tina4_python.core.router import Router, _routes
91
-
92
- # Remember route count before
93
- before = len(_routes)
94
-
95
- # Reload all src/ modules that are already in sys.modules
96
- root = Path("src").resolve()
97
- if not root.is_dir():
98
- return
99
-
100
- skip = {"public", "templates", "scss", "locales", "icons"}
101
- reloaded = 0
102
-
103
- # Clear existing routes (they'll be re-registered on reload)
104
- _routes.clear()
105
-
106
- for py_file in sorted(root.rglob("*.py")):
107
- if any(part.startswith("_") for part in py_file.parts):
108
- continue
109
- if any(s in py_file.parts for s in skip):
110
- continue
111
-
112
- try:
113
- rel = py_file.relative_to(Path.cwd()).with_suffix("")
114
- module_name = ".".join(rel.parts)
115
-
116
- if module_name in sys.modules:
117
- # Reload existing module
118
- importlib.reload(sys.modules[module_name])
119
- reloaded += 1
120
- else:
121
- # Import new module
122
- importlib.import_module(module_name)
123
- reloaded += 1
124
- except Exception as e:
125
- Log.error(f"DevReload: failed to reload {py_file}: {e}")
126
-
127
- # Re-register built-in routes (health check)
128
- from tina4_python.core.server import _health_handler
129
- Router.add("GET", "/health", _health_handler)
130
-
131
- after = len(_routes)
132
- Log.debug(f"DevReload: reloaded {reloaded} modules, {before} -> {after} routes")
133
-
134
-
135
- def _poll_loop(directories: list[str], interval: float = 1.0):
136
- """Background thread that polls file mtimes and triggers re-discovery."""
137
- global _last_mtime, _last_change_file, _running
138
-
139
- # Initial scan
140
- with _lock:
141
- _last_mtime, _last_change_file = _scan_mtime(directories)
142
-
143
- Log.debug(f"DevReload: watching {', '.join(directories)} "
144
- f"(extensions: {', '.join(sorted(_WATCH_EXTENSIONS))})")
145
-
146
- while _running:
147
- time.sleep(interval)
148
-
149
- new_mtime, new_file = _scan_mtime(directories)
150
-
151
- if new_mtime > _last_mtime:
152
- rel_path = new_file
153
- try:
154
- rel_path = str(Path(new_file).relative_to(Path.cwd()))
155
- except ValueError:
156
- pass
157
-
158
- Log.info(f"DevReload: change detected in {rel_path}")
159
-
160
- with _lock:
161
- _last_mtime = new_mtime
162
- _last_change_file = new_file
163
-
164
- # Re-discover routes if a Python file changed
165
- if new_file.endswith(".py"):
166
- try:
167
- _rediscover_routes()
168
- except Exception as e:
169
- Log.error(f"DevReload: route re-discovery failed: {e}")
170
-
171
- # Note: SCSS compilation is handled by the tina4 CLI (Rust).
172
- # DevReload only handles route re-discovery and browser refresh.
173
-
174
-
175
- def start(directories: list[str] | None = None, interval: float | None = None):
176
- """Start the DevReload file watcher in a background thread.
177
-
178
- Args:
179
- directories: List of directories to watch. Defaults to ["src", "public"].
180
- interval: Polling interval in seconds. Defaults to TINA4_DEV_POLL_INTERVAL/1000
181
- env var (milliseconds), or 3.0 seconds if not set.
182
- """
183
- global _running
184
-
185
- if _running:
186
- return
187
-
188
- if directories is None:
189
- directories = ["src", "public"]
190
-
191
- if interval is None:
192
- env_ms = os.environ.get("TINA4_DEV_POLL_INTERVAL", "3000")
193
- try:
194
- interval = max(0.5, int(env_ms) / 1000.0)
195
- except ValueError:
196
- interval = 3.0
197
-
198
- _running = True
199
-
200
- thread = threading.Thread(
201
- target=_poll_loop,
202
- args=(directories, interval),
203
- daemon=True,
204
- name="tina4-dev-reload",
205
- )
206
- thread.start()
207
- Log.info(f"DevReload: file watcher started (interval={interval:.1f}s)")
208
-
209
-
210
- def stop():
211
- """Stop the DevReload file watcher."""
212
- global _running
213
- _running = False
214
- Log.debug("DevReload: file watcher stopped")
File without changes
File without changes