tina4-python 3.10.41__tar.gz → 3.10.42__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 (143) hide show
  1. {tina4_python-3.10.41 → tina4_python-3.10.42}/PKG-INFO +2 -2
  2. {tina4_python-3.10.41 → tina4_python-3.10.42}/README.md +1 -1
  3. {tina4_python-3.10.41 → tina4_python-3.10.42}/pyproject.toml +1 -1
  4. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/__init__.py +2 -2
  5. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/server.py +69 -70
  6. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/dev_admin/__init__.py +9 -0
  7. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/dev_admin/metrics.py +18 -0
  8. {tina4_python-3.10.41 → tina4_python-3.10.42}/.gitignore +0 -0
  9. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/CLAUDE.md +0 -0
  10. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/__init__.py +0 -0
  13. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/ai/__init__.py +0 -0
  14. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/cli/__init__.py +0 -0
  18. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/core/router.py +0 -0
  26. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/dev_reload.py +0 -0
  39. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/dotenv/__init__.py +0 -0
  40. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/frond/FROND.md +0 -0
  41. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/frond/__init__.py +0 -0
  42. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/frond/engine.py +0 -0
  43. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/auth/meta.json +0 -0
  44. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  45. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/database/meta.json +0 -0
  46. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  47. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/meta.json +0 -0
  48. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  49. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/orm/meta.json +0 -0
  50. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  51. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  52. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/queue/meta.json +0 -0
  53. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  54. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/meta.json +0 -0
  55. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  56. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/templates/meta.json +0 -0
  57. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  58. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  59. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/graphql/__init__.py +0 -0
  60. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/i18n/__init__.py +0 -0
  61. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/mcp/__init__.py +0 -0
  62. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/mcp/protocol.py +0 -0
  63. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/mcp/tools.py +0 -0
  64. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/messenger/__init__.py +0 -0
  65. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/migration/__init__.py +0 -0
  66. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/migration/runner.py +0 -0
  67. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/orm/__init__.py +0 -0
  68. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/orm/fields.py +0 -0
  69. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/orm/model.py +0 -0
  70. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/css/tina4.css +0 -0
  71. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/css/tina4.min.css +0 -0
  72. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/favicon.ico +0 -0
  73. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/images/logo.svg +0 -0
  74. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  75. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/js/frond.min.js +0 -0
  76. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  77. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/js/tina4.min.js +0 -0
  78. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/js/tina4js.min.js +0 -0
  79. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/swagger/index.html +0 -0
  80. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  81. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/query_builder/__init__.py +0 -0
  82. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/queue/__init__.py +0 -0
  83. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/queue_backends/__init__.py +0 -0
  84. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/queue_backends/kafka_backend.py +0 -0
  85. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/queue_backends/mongo_backend.py +0 -0
  86. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  87. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/__init__.py +0 -0
  88. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  89. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_badges.scss +0 -0
  90. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  91. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_cards.scss +0 -0
  92. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_forms.scss +0 -0
  93. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_grid.scss +0 -0
  94. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_modals.scss +0 -0
  95. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_nav.scss +0 -0
  96. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_reset.scss +0 -0
  97. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_tables.scss +0 -0
  98. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_typography.scss +0 -0
  99. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  100. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_variables.scss +0 -0
  101. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/base.scss +0 -0
  102. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/colors.scss +0 -0
  103. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/scss/tina4css/tina4.scss +0 -0
  104. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/seeder/__init__.py +0 -0
  105. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/service/__init__.py +0 -0
  106. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/session/__init__.py +0 -0
  107. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/session_handlers/__init__.py +0 -0
  108. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  109. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/session_handlers/redis_handler.py +0 -0
  110. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/session_handlers/valkey_handler.py +0 -0
  111. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/swagger/__init__.py +0 -0
  112. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/components/crud.twig +0 -0
  113. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  114. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  115. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/docker/python/Dockerfile +0 -0
  116. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  117. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/302.twig +0 -0
  118. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/401.twig +0 -0
  119. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/403.twig +0 -0
  120. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/404.twig +0 -0
  121. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/500.twig +0 -0
  122. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/502.twig +0 -0
  123. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/503.twig +0 -0
  124. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/errors/base.twig +0 -0
  125. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/frontend/README.md +0 -0
  126. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/templates/readme.md +0 -0
  127. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/test_client/__init__.py +0 -0
  128. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  137. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  138. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/validator/__init__.py +0 -0
  141. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/websocket/__init__.py +0 -0
  142. {tina4_python-3.10.41 → tina4_python-3.10.42}/tina4_python/websocket/backplane.py +0 -0
  143. {tina4_python-3.10.41 → tina4_python-3.10.42}/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.41
3
+ Version: 3.10.42
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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
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%2C066%20passing-brightgreen" alt="Tests">
45
+ <img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
46
46
  <img src="https://img.shields.io/badge/features-54-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>
@@ -10,7 +10,7 @@
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%2C066%20passing-brightgreen" alt="Tests">
13
+ <img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
14
14
  <img src="https://img.shields.io/badge/features-54-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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.10.41"
3
+ version = "3.10.42"
4
4
  description = "Tina4 for Python — 54 built-in features, zero dependencies"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -19,7 +19,7 @@ from tina4_python.core.router import (
19
19
  from tina4_python.core.middleware import CorsMiddleware, RateLimiter
20
20
  from tina4_python.core.cache import Cache
21
21
  from tina4_python.core.events import on, off, emit, emit_async, once, listeners, events, clear as clear_events
22
- from tina4_python.core.server import run, resolve_config
22
+ from tina4_python.core.server import run, resolve_config, handle
23
23
 
24
24
  __all__ = [
25
25
  "Request", "Response", "Router",
@@ -28,5 +28,5 @@ __all__ = [
28
28
  "CorsMiddleware", "RateLimiter",
29
29
  "Cache",
30
30
  "on", "off", "emit", "emit_async", "once", "listeners", "events", "clear_events",
31
- "run", "resolve_config",
31
+ "run", "resolve_config", "handle",
32
32
  ]
@@ -607,54 +607,33 @@ async def _handle_dev_websocket(reader, writer, headers, path):
607
607
  pass
608
608
 
609
609
 
610
- async def app(scope: dict, receive, send):
611
- """ASGI entry point — compatible with uvicorn, hypercorn, granian."""
612
- if scope["type"] == "lifespan":
613
- msg = await receive()
614
- if msg["type"] == "lifespan.startup":
615
- import time
616
- global _start_time
617
- _start_time = time.time()
618
- await send({"type": "lifespan.startup.complete"})
619
- elif msg["type"] == "lifespan.shutdown":
620
- await send({"type": "lifespan.shutdown.complete"})
621
- return
622
-
623
- if scope["type"] == "websocket":
624
- await _handle_asgi_websocket(scope, receive, send)
625
- return
626
-
627
- if scope["type"] != "http":
628
- return
629
610
 
630
- # Read full body
631
- body = b""
632
- while True:
633
- msg = await receive()
634
- body += msg.get("body", b"")
635
- if not msg.get("more_body", False):
636
- break
611
+ async def handle(request: Request) -> Response:
612
+ """Dispatch a pre-built Request through the Tina4 router and return a Response.
637
613
 
638
- # Build request
639
- request = Request.from_scope(scope, body)
614
+ Handles session setup, CORS, rate limiting, routing, auth, middleware,
615
+ dev toolbar injection, and session saving. The caller is responsible
616
+ for sending the response over the wire. Useful for testing and embedding.
617
+ """
640
618
  request_id = request.headers.get("x-request-id", str(uuid.uuid4())[:8])
641
619
  set_request_id(request_id)
642
620
 
643
621
  # Auto-start session — lazy, reads cookie, saves on response
644
- try:
645
- from tina4_python.session import Session
646
- cookie_header = dict(scope.get("headers", [])).get(b"cookie", b"").decode()
647
- sid_match = None
648
- for part in cookie_header.split(";"):
649
- part = part.strip()
650
- if part.startswith("tina4_session="):
651
- sid_match = part.split("=", 1)[1]
652
- break
653
- sess = Session()
654
- sess.start(sid_match)
655
- request.session = sess
656
- except Exception:
657
- pass # Session module not available — session stays None
622
+ if request.session is None:
623
+ try:
624
+ from tina4_python.session import Session
625
+ cookie_header = request.headers.get("cookie", "")
626
+ sid_match = None
627
+ for part in cookie_header.split(";"):
628
+ part = part.strip()
629
+ if part.startswith("tina4_session="):
630
+ sid_match = part.split("=", 1)[1]
631
+ break
632
+ sess = Session()
633
+ sess.start(sid_match)
634
+ request.session = sess
635
+ except Exception:
636
+ pass # Session module not available — session stays None
658
637
 
659
638
  response = Response()
660
639
  response.header("x-request-id", request_id)
@@ -663,9 +642,7 @@ async def app(scope: dict, receive, send):
663
642
  if _cors.is_preflight(request):
664
643
  _cors.apply(request, response)
665
644
  response.status(204)
666
- await send({"type": "http.response.start", "status": 204, "headers": response.build_headers("")})
667
- await send({"type": "http.response.body", "body": b""})
668
- return
645
+ return response
669
646
 
670
647
  # Rate limiting
671
648
  rate_enabled = os.environ.get("TINA4_RATE_LIMIT", "")
@@ -680,9 +657,7 @@ async def app(scope: dict, receive, send):
680
657
  "status": 429,
681
658
  })
682
659
  response.header("retry-after", str(info["reset"]))
683
- await send({"type": "http.response.start", "status": 429, "headers": response.build_headers("")})
684
- await send({"type": "http.response.body", "body": response.content})
685
- return
660
+ return response
686
661
 
687
662
  import time as _time
688
663
  _req_start = _time.perf_counter()
@@ -727,12 +702,9 @@ async def app(scope: dict, receive, send):
727
702
  else:
728
703
  response.status(404).json({"error": "Not found"})
729
704
 
730
- # Send dev admin response (skip overlay injection)
705
+ # Dev admin response (skip overlay injection)
731
706
  _cors.apply(request, response)
732
- headers = response.build_headers("")
733
- await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
734
- await send({"type": "http.response.body", "body": response.content})
735
- return
707
+ return response
736
708
 
737
709
  # Swagger auto-register: serve /swagger and /swagger/openapi.json when debug is on
738
710
  if _is_dev and request.method == "GET":
@@ -749,10 +721,7 @@ async def app(scope: dict, receive, send):
749
721
  )
750
722
  response.html(swagger_html)
751
723
  _cors.apply(request, response)
752
- headers = response.build_headers("")
753
- await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
754
- await send({"type": "http.response.body", "body": response.content})
755
- return
724
+ return response
756
725
  elif request.path == "/swagger/openapi.json":
757
726
  # Serve OpenAPI spec JSON from all registered routes
758
727
  from tina4_python.swagger import Swagger as _SwaggerGen
@@ -760,10 +729,7 @@ async def app(scope: dict, receive, send):
760
729
  _spec = _swagger.generate(Router.get_routes())
761
730
  response.json(_spec)
762
731
  _cors.apply(request, response)
763
- headers = response.build_headers("")
764
- await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
765
- await send({"type": "http.response.body", "body": response.content})
766
- return
732
+ return response
767
733
 
768
734
  # Match route
769
735
  route, params = Router.match(request.method, request.path)
@@ -931,13 +897,13 @@ async def app(scope: dict, receive, send):
931
897
  request.method, request.path, matched_pattern,
932
898
  request_id, len(Router.get_routes()),
933
899
  ).encode()
934
- content = response.content
900
+ content_body = response.content
935
901
  # Inject before </body> if present, else append
936
- if b"</body>" in content:
937
- content = content.replace(b"</body>", toolbar + b"\n</body>", 1)
902
+ if b"</body>" in content_body:
903
+ content_body = content_body.replace(b"</body>", toolbar + b"\n</body>", 1)
938
904
  else:
939
- content = content + toolbar
940
- response.content = content
905
+ content_body = content_body + toolbar
906
+ response.content = content_body
941
907
  except Exception:
942
908
  pass
943
909
 
@@ -954,9 +920,6 @@ async def app(scope: dict, receive, send):
954
920
  except Exception:
955
921
  pass
956
922
 
957
- # ETag check — 304 Not Modified
958
- if_none_match = request.headers.get("if-none-match", "")
959
-
960
923
  # Save session and set cookie if session was used
961
924
  if request.session is not None:
962
925
  try:
@@ -973,7 +936,43 @@ async def app(scope: dict, receive, send):
973
936
  except Exception:
974
937
  pass
975
938
 
976
- # Build and send response
939
+ return response
940
+
941
+
942
+ async def app(scope: dict, receive, send):
943
+ """ASGI entry point — compatible with uvicorn, hypercorn, granian."""
944
+ if scope["type"] == "lifespan":
945
+ msg = await receive()
946
+ if msg["type"] == "lifespan.startup":
947
+ import time
948
+ global _start_time
949
+ _start_time = time.time()
950
+ await send({"type": "lifespan.startup.complete"})
951
+ elif msg["type"] == "lifespan.shutdown":
952
+ await send({"type": "lifespan.shutdown.complete"})
953
+ return
954
+
955
+ if scope["type"] == "websocket":
956
+ await _handle_asgi_websocket(scope, receive, send)
957
+ return
958
+
959
+ if scope["type"] != "http":
960
+ return
961
+
962
+ # Read full body
963
+ body = b""
964
+ while True:
965
+ msg = await receive()
966
+ body += msg.get("body", b"")
967
+ if not msg.get("more_body", False):
968
+ break
969
+
970
+ # Build request and dispatch
971
+ request = Request.from_scope(scope, body)
972
+ response = await handle(request)
973
+
974
+ # ETag check — 304 Not Modified
975
+ if_none_match = request.headers.get("if-none-match", "")
977
976
  accept_encoding = request.headers.get("accept-encoding", "")
978
977
  headers = response.build_headers(accept_encoding)
979
978
 
@@ -1924,6 +1924,15 @@ function drillDownFile(path){
1924
1924
  });
1925
1925
  html+='</div>';
1926
1926
  }
1927
+ if(d.warnings&&d.warnings.length){
1928
+ html+='<h3 style="margin:0.75rem 0 0.25rem;color:#eab308;font-size:0.85rem">&#9888; Warnings</h3>';
1929
+ html+='<div style="display:flex;flex-direction:column;gap:4px">';
1930
+ d.warnings.forEach(function(w){
1931
+ html+='<div style="padding:4px 8px;background:rgba(234,179,8,0.08);border-left:3px solid #eab308;border-radius:0 4px 4px 0;font-size:0.75rem;font-family:var(--mono);color:var(--text)">';
1932
+ html+='<span style="color:#eab308;margin-right:6px">L'+w.line+'</span>'+w.message+'</div>';
1933
+ });
1934
+ html+='</div>';
1935
+ }
1927
1936
  dd.querySelector('.p-md').innerHTML=html;
1928
1937
  }).catch(function(e){
1929
1938
  dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
@@ -327,6 +327,23 @@ def file_detail(file_path: str) -> dict:
327
327
  classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
328
328
  imports = _extract_imports(tree, file_path)
329
329
 
330
+ # Detect empty methods/functions (body is only `pass` or a docstring)
331
+ warnings = []
332
+ for node in ast.walk(tree):
333
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
334
+ body = node.body
335
+ # Strip leading docstring
336
+ effective = body[1:] if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) else body
337
+ if not effective or all(isinstance(s, ast.Pass) for s in effective):
338
+ parent = _get_parent_class(tree, node)
339
+ name = f"{parent}.{node.name}" if parent else node.name
340
+ warnings.append({"type": "empty_method", "message": f"Method '{name}' appears to be empty", "line": node.lineno})
341
+ elif isinstance(node, ast.ClassDef):
342
+ body = node.body
343
+ effective = body[1:] if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) else body
344
+ if not effective or all(isinstance(s, ast.Pass) for s in effective):
345
+ warnings.append({"type": "empty_class", "message": f"Class '{node.name}' appears to be empty", "line": node.lineno})
346
+
330
347
  return {
331
348
  "path": file_path,
332
349
  "loc": loc,
@@ -334,6 +351,7 @@ def file_detail(file_path: str) -> dict:
334
351
  "classes": classes,
335
352
  "functions": functions,
336
353
  "imports": imports,
354
+ "warnings": warnings,
337
355
  }
338
356
 
339
357