tina4-python 3.10.40__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.40 → tina4_python-3.10.42}/PKG-INFO +53 -36
  2. {tina4_python-3.10.40 → tina4_python-3.10.42}/README.md +51 -34
  3. {tina4_python-3.10.40 → tina4_python-3.10.42}/pyproject.toml +2 -2
  4. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/CLAUDE.md +2 -2
  5. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/__init__.py +2 -2
  6. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/server.py +69 -70
  7. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_admin/__init__.py +9 -0
  8. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_admin/metrics.py +18 -0
  9. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/model.py +7 -8
  10. {tina4_python-3.10.40 → tina4_python-3.10.42}/.gitignore +0 -0
  11. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/__init__.py +0 -0
  14. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_reload.py +0 -0
  40. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/engine.py +0 -0
  44. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/__init__.py +0 -0
  63. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/protocol.py +0 -0
  64. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/tools.py +0 -0
  65. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/messenger/__init__.py +0 -0
  66. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/migration/__init__.py +0 -0
  67. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/migration/runner.py +0 -0
  68. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/__init__.py +0 -0
  69. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/fields.py +0 -0
  70. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/css/tina4.css +0 -0
  71. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/css/tina4.min.css +0 -0
  72. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/favicon.ico +0 -0
  73. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/images/logo.svg +0 -0
  74. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  75. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/frond.min.js +0 -0
  76. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  77. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4.min.js +0 -0
  78. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4js.min.js +0 -0
  79. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/swagger/index.html +0 -0
  80. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  81. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/query_builder/__init__.py +0 -0
  82. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue/__init__.py +0 -0
  83. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/__init__.py +0 -0
  84. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/kafka_backend.py +0 -0
  85. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/mongo_backend.py +0 -0
  86. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  87. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/__init__.py +0 -0
  88. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  89. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_badges.scss +0 -0
  90. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  91. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_cards.scss +0 -0
  92. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_forms.scss +0 -0
  93. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_grid.scss +0 -0
  94. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_modals.scss +0 -0
  95. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_nav.scss +0 -0
  96. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_reset.scss +0 -0
  97. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_tables.scss +0 -0
  98. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_typography.scss +0 -0
  99. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  100. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_variables.scss +0 -0
  101. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/base.scss +0 -0
  102. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/colors.scss +0 -0
  103. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/tina4.scss +0 -0
  104. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/seeder/__init__.py +0 -0
  105. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/service/__init__.py +0 -0
  106. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session/__init__.py +0 -0
  107. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/__init__.py +0 -0
  108. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  109. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/redis_handler.py +0 -0
  110. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/valkey_handler.py +0 -0
  111. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/swagger/__init__.py +0 -0
  112. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/components/crud.twig +0 -0
  113. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  114. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  115. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/python/Dockerfile +0 -0
  116. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  117. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/302.twig +0 -0
  118. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/401.twig +0 -0
  119. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/403.twig +0 -0
  120. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/404.twig +0 -0
  121. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/500.twig +0 -0
  122. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/502.twig +0 -0
  123. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/503.twig +0 -0
  124. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/base.twig +0 -0
  125. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/frontend/README.md +0 -0
  126. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/readme.md +0 -0
  127. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/test_client/__init__.py +0 -0
  128. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  137. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  138. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/validator/__init__.py +0 -0
  141. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/websocket/__init__.py +0 -0
  142. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/websocket/backplane.py +0 -0
  143. {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.40
4
- Summary: Tina4 Python v3 Zero-dependency, lightweight web framework
3
+ Version: 3.10.42
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
7
7
  Requires-Python: >=3.12
@@ -35,16 +35,15 @@ Description-Content-Type: text/markdown
35
35
  </p>
36
36
 
37
37
  <h1 align="center">Tina4 Python</h1>
38
- <h3 align="center">This Is Now A 4Framework</h3>
39
38
 
40
39
  <p align="center">
41
- Laravel joy. Python speed. 10x less code. Zero third-party dependencies.
40
+ 54 built-in features. Zero dependencies. One import, everything works.
42
41
  </p>
43
42
 
44
43
  <p align="center">
45
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>
46
- <img src="https://img.shields.io/badge/tests-1%2C791%20passing-brightgreen" alt="Tests">
47
- <img src="https://img.shields.io/badge/features-38-blue" alt="Features">
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">
48
47
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
49
48
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
50
49
  </p>
@@ -54,6 +53,7 @@ Description-Content-Type: text/markdown
54
53
  <a href="#getting-started">Getting Started</a> &bull;
55
54
  <a href="#features">Features</a> &bull;
56
55
  <a href="#cli-reference">CLI Reference</a> &bull;
56
+ <a href="#cross-framework-parity">Parity</a> &bull;
57
57
  <a href="https://tina4.com">tina4.com</a>
58
58
  </p>
59
59
 
@@ -102,27 +102,24 @@ Open http://localhost:7145
102
102
 
103
103
  ---
104
104
 
105
- ## What's Included
105
+ ## What's Built In (54 Features)
106
106
 
107
107
  Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
108
108
 
109
109
  | Category | Features |
110
110
  |----------|----------|
111
- | **HTTP** | ASGI server, decorator routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
112
- | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
113
- | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
114
- | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
115
- | **Auth** | Zero-dep JWT (HS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
116
- | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
117
- | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
118
- | **Real-time** | Native asyncio WebSocket (RFC 6455), per-path routing, connection manager |
119
- | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
120
- | **DX** | Dev admin dashboard (11 tabs), error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
121
- | **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
122
- | **Mail** | SMTP send (plain/HTML/attachments), IMAP read/search, dev mailbox capture |
123
- | **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages, HTML element builder |
124
-
125
- **1,633 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
111
+ | **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
112
+ | **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird — unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
113
+ | **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
114
+ | **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
115
+ | **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8x faster), SCSS auto-compilation, built-in CSS (~24 KB) |
116
+ | **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
117
+ | **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters — service runner — event system (on/emit/once/off) |
118
+ | **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
119
+ | **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
+ | **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
+
122
+ **2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
126
123
 
127
124
  For full documentation visit **[tina4.com](https://tina4.com)**.
128
125
 
@@ -712,23 +709,43 @@ tina4python ai --all # Install for ALL supported tools
712
709
 
713
710
  Supported: Claude Code, Cursor, GitHub Copilot, Windsurf, Aider, Cline, OpenAI Codex CLI. Generates framework-aware context so AI assistants understand Tina4's conventions.
714
711
 
715
- ## Carbonah Green Benchmarks
712
+ ## Performance
716
713
 
717
- All 9 benchmarks rated **A+** (South Africa grid, 1000 iterations each):
714
+ Benchmarked with `wrk` 5,000 requests, 50 concurrent, median of 3 runs:
718
715
 
719
- | Benchmark | SCI (gCO2eq) | Grade |
720
- |-----------|-------------|-------|
721
- | JSON Hello World | 0.000864 | A+ |
722
- | Single DB Query | 0.000538 | A+ |
723
- | Multiple DB Queries | 0.001350 | A+ |
724
- | Template Rendering | 0.003237 | A+ |
725
- | Large JSON Payload | 0.000983 | A+ |
726
- | Plaintext Response | 0.000377 | A+ |
727
- | CRUD Cycle | 0.000456 | A+ |
728
- | Paginated Query | 0.000990 | A+ |
729
- | Framework Startup | 0.000801 | A+ |
716
+ | Framework | JSON req/s | Deps | Features |
717
+ |-----------|-----------|------|----------|
718
+ | **Tina4 Python** | **6,508** | 0 | 54 |
719
+ | FastAPI | 12,652 | 12+ | ~8 |
720
+ | Flask | 4,928 | 6+ | ~7 |
721
+ | Bottle | 4,355 | 0 | ~5 |
722
+ | Django | 4,050 | 20+ | ~22 |
730
723
 
731
- Run locally: `python benchmarks/run_carbonah.py`
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.
725
+
726
+ **Across all 4 Tina4 implementations:**
727
+
728
+ | | Python | PHP | Ruby | Node.js |
729
+ |---|--------|-----|------|---------|
730
+ | **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
731
+ | **Dependencies** | 0 | 0 | 0 | 0 |
732
+ | **Features** | 54 | 54 | 54 | 54 |
733
+
734
+ Run benchmarks locally: `python benchmarks/benchmark.py --python`
735
+
736
+ ---
737
+
738
+ ## Cross-Framework Parity
739
+
740
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
741
+
742
+ | | Python | PHP | Ruby | Node.js |
743
+ |---|--------|-----|------|---------|
744
+ | **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 |
747
+
748
+ **7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
732
749
 
733
750
  ---
734
751
 
@@ -3,16 +3,15 @@
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Tina4 Python</h1>
6
- <h3 align="center">This Is Now A 4Framework</h3>
7
6
 
8
7
  <p align="center">
9
- Laravel joy. Python speed. 10x less code. Zero third-party dependencies.
8
+ 54 built-in features. Zero dependencies. One import, everything works.
10
9
  </p>
11
10
 
12
11
  <p align="center">
13
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>
14
- <img src="https://img.shields.io/badge/tests-1%2C791%20passing-brightgreen" alt="Tests">
15
- <img src="https://img.shields.io/badge/features-38-blue" alt="Features">
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">
16
15
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
16
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
18
17
  </p>
@@ -22,6 +21,7 @@
22
21
  <a href="#getting-started">Getting Started</a> &bull;
23
22
  <a href="#features">Features</a> &bull;
24
23
  <a href="#cli-reference">CLI Reference</a> &bull;
24
+ <a href="#cross-framework-parity">Parity</a> &bull;
25
25
  <a href="https://tina4.com">tina4.com</a>
26
26
  </p>
27
27
 
@@ -70,27 +70,24 @@ Open http://localhost:7145
70
70
 
71
71
  ---
72
72
 
73
- ## What's Included
73
+ ## What's Built In (54 Features)
74
74
 
75
75
  Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
76
76
 
77
77
  | Category | Features |
78
78
  |----------|----------|
79
- | **HTTP** | ASGI server, decorator routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
80
- | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
81
- | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
82
- | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
83
- | **Auth** | Zero-dep JWT (HS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
84
- | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
85
- | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
86
- | **Real-time** | Native asyncio WebSocket (RFC 6455), per-path routing, connection manager |
87
- | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
88
- | **DX** | Dev admin dashboard (11 tabs), error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
89
- | **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
90
- | **Mail** | SMTP send (plain/HTML/attachments), IMAP read/search, dev mailbox capture |
91
- | **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages, HTML element builder |
92
-
93
- **1,633 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
79
+ | **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
80
+ | **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird — unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
81
+ | **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
82
+ | **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
83
+ | **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8x faster), SCSS auto-compilation, built-in CSS (~24 KB) |
84
+ | **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
85
+ | **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters — service runner — event system (on/emit/once/off) |
86
+ | **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
87
+ | **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
+ | **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
+
90
+ **2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
94
91
 
95
92
  For full documentation visit **[tina4.com](https://tina4.com)**.
96
93
 
@@ -680,23 +677,43 @@ tina4python ai --all # Install for ALL supported tools
680
677
 
681
678
  Supported: Claude Code, Cursor, GitHub Copilot, Windsurf, Aider, Cline, OpenAI Codex CLI. Generates framework-aware context so AI assistants understand Tina4's conventions.
682
679
 
683
- ## Carbonah Green Benchmarks
680
+ ## Performance
684
681
 
685
- All 9 benchmarks rated **A+** (South Africa grid, 1000 iterations each):
682
+ Benchmarked with `wrk` 5,000 requests, 50 concurrent, median of 3 runs:
686
683
 
687
- | Benchmark | SCI (gCO2eq) | Grade |
688
- |-----------|-------------|-------|
689
- | JSON Hello World | 0.000864 | A+ |
690
- | Single DB Query | 0.000538 | A+ |
691
- | Multiple DB Queries | 0.001350 | A+ |
692
- | Template Rendering | 0.003237 | A+ |
693
- | Large JSON Payload | 0.000983 | A+ |
694
- | Plaintext Response | 0.000377 | A+ |
695
- | CRUD Cycle | 0.000456 | A+ |
696
- | Paginated Query | 0.000990 | A+ |
697
- | Framework Startup | 0.000801 | A+ |
684
+ | Framework | JSON req/s | Deps | Features |
685
+ |-----------|-----------|------|----------|
686
+ | **Tina4 Python** | **6,508** | 0 | 54 |
687
+ | FastAPI | 12,652 | 12+ | ~8 |
688
+ | Flask | 4,928 | 6+ | ~7 |
689
+ | Bottle | 4,355 | 0 | ~5 |
690
+ | Django | 4,050 | 20+ | ~22 |
698
691
 
699
- Run locally: `python benchmarks/run_carbonah.py`
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.
693
+
694
+ **Across all 4 Tina4 implementations:**
695
+
696
+ | | Python | PHP | Ruby | Node.js |
697
+ |---|--------|-----|------|---------|
698
+ | **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
699
+ | **Dependencies** | 0 | 0 | 0 | 0 |
700
+ | **Features** | 54 | 54 | 54 | 54 |
701
+
702
+ Run benchmarks locally: `python benchmarks/benchmark.py --python`
703
+
704
+ ---
705
+
706
+ ## Cross-Framework Parity
707
+
708
+ Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
709
+
710
+ | | Python | PHP | Ruby | Node.js |
711
+ |---|--------|-----|------|---------|
712
+ | **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 |
715
+
716
+ **7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
700
717
 
701
718
  ---
702
719
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.10.40"
4
- description = "Tina4 Python v3 Zero-dependency, lightweight web framework"
3
+ version = "3.10.42"
4
+ description = "Tina4 for Python — 54 built-in features, zero dependencies"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
7
7
  ]
@@ -1762,8 +1762,8 @@ async def dashboard(request, response):
1762
1762
 
1763
1763
  ## v3 Features Summary
1764
1764
 
1765
- - **38 built-in features**, zero third-party dependencies
1766
- - **1,633 tests** passing across all modules
1765
+ - **54 built-in features**, zero third-party dependencies
1766
+ - **2,066 tests** passing across all modules
1767
1767
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1768
1768
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1769
1769
  - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
@@ -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
 
@@ -325,7 +325,6 @@ class ORM(metaclass=ORMMeta):
325
325
  pk_value: Primary key value.
326
326
  include: List of relationship names to eager-load.
327
327
  """
328
- db = cls._get_db()
329
328
  pk = cls._get_pk()
330
329
  table = cls._get_table()
331
330
  pk_col = cls.field_mapping.get(pk, cls._fields[pk].column)
@@ -334,13 +333,7 @@ class ORM(metaclass=ORMMeta):
334
333
  if cls.soft_delete:
335
334
  sql += " AND deleted_at IS NULL"
336
335
 
337
- row = db.fetch_one(sql, [pk_value])
338
- if row is None:
339
- return None
340
- instance = cls(row)
341
- if include:
342
- cls._eager_load([instance], include)
343
- return instance
336
+ return cls.select_one(sql, [pk_value], include=include)
344
337
 
345
338
  @classmethod
346
339
  def find(cls, pk_value, include: list[str] = None):
@@ -395,6 +388,12 @@ class ORM(metaclass=ORMMeta):
395
388
  cls._eager_load(instances, include)
396
389
  return instances, result.count
397
390
 
391
+ @classmethod
392
+ def select_one(cls, sql: str, params: list = None, include: list[str] = None):
393
+ """Return a single ORM instance for a raw SQL query, or None if no rows match."""
394
+ instances, _ = cls.select(sql, params, limit=1, offset=0, include=include)
395
+ return instances[0] if instances else None
396
+
398
397
  @classmethod
399
398
  def where(cls, filter_sql: str, params: list = None, limit: int = 20, offset: int = 0,
400
399
  include: list[str] = None):