tina4-python 3.8.2__tar.gz → 3.8.7__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 (138) hide show
  1. {tina4_python-3.8.2 → tina4_python-3.8.7}/.gitignore +4 -1
  2. {tina4_python-3.8.2 → tina4_python-3.8.7}/PKG-INFO +9 -10
  3. {tina4_python-3.8.2 → tina4_python-3.8.7}/README.md +8 -9
  4. {tina4_python-3.8.2 → tina4_python-3.8.7}/pyproject.toml +1 -1
  5. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/server.py +52 -8
  7. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/orm/model.py +13 -0
  8. tina4_python-3.8.7/tina4_python/query_builder/__init__.py +280 -0
  9. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/session/__init__.py +4 -1
  10. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/CLAUDE.md +0 -0
  11. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/ai/__init__.py +0 -0
  14. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/cli/__init__.py +0 -0
  18. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/dev_reload.py +0 -0
  41. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/dotenv/__init__.py +0 -0
  42. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/frond/FROND.md +0 -0
  43. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/frond/__init__.py +0 -0
  44. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/frond/engine.py +0 -0
  45. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/auth/meta.json +0 -0
  46. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  47. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/database/meta.json +0 -0
  48. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  49. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/error-overlay/meta.json +0 -0
  50. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  51. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/orm/meta.json +0 -0
  52. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  53. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  54. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/queue/meta.json +0 -0
  55. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  56. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/rest-api/meta.json +0 -0
  57. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  58. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/templates/meta.json +0 -0
  59. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  60. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  61. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/graphql/__init__.py +0 -0
  62. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/i18n/__init__.py +0 -0
  63. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/messenger/__init__.py +0 -0
  64. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/migration/__init__.py +0 -0
  65. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/migration/runner.py +0 -0
  66. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/orm/__init__.py +0 -0
  67. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/orm/fields.py +0 -0
  68. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/css/tina4.css +0 -0
  69. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/css/tina4.min.css +0 -0
  70. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/favicon.ico +0 -0
  71. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/images/logo.svg +0 -0
  72. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  73. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/js/frond.min.js +0 -0
  74. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  75. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/js/tina4.min.js +0 -0
  76. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/js/tina4js.min.js +0 -0
  77. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/swagger/index.html +0 -0
  78. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  79. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/queue/__init__.py +0 -0
  80. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/queue_backends/__init__.py +0 -0
  81. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/queue_backends/kafka_backend.py +0 -0
  82. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/queue_backends/mongo_backend.py +0 -0
  83. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  84. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/__init__.py +0 -0
  85. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  86. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_badges.scss +0 -0
  87. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  88. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_cards.scss +0 -0
  89. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_forms.scss +0 -0
  90. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_grid.scss +0 -0
  91. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_modals.scss +0 -0
  92. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_nav.scss +0 -0
  93. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_reset.scss +0 -0
  94. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_tables.scss +0 -0
  95. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_typography.scss +0 -0
  96. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  97. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/_variables.scss +0 -0
  98. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/base.scss +0 -0
  99. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/colors.scss +0 -0
  100. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/scss/tina4css/tina4.scss +0 -0
  101. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/seeder/__init__.py +0 -0
  102. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/service/__init__.py +0 -0
  103. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/session_handlers/__init__.py +0 -0
  104. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  105. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/session_handlers/redis_handler.py +0 -0
  106. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/session_handlers/valkey_handler.py +0 -0
  107. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/swagger/__init__.py +0 -0
  108. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/components/crud.twig +0 -0
  109. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  110. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  111. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/docker/python/Dockerfile +0 -0
  112. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  113. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/302.twig +0 -0
  114. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/401.twig +0 -0
  115. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/403.twig +0 -0
  116. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/404.twig +0 -0
  117. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/500.twig +0 -0
  118. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/502.twig +0 -0
  119. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/503.twig +0 -0
  120. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/errors/base.twig +0 -0
  121. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/frontend/README.md +0 -0
  122. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/templates/readme.md +0 -0
  123. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/test_client/__init__.py +0 -0
  124. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  125. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  126. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  127. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  128. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/validator/__init__.py +0 -0
  137. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/websocket/__init__.py +0 -0
  138. {tina4_python-3.8.2 → tina4_python-3.8.7}/tina4_python/wsdl/__init__.py +0 -0
@@ -54,7 +54,8 @@ __pycache__/
54
54
  /benchmarks/secrets/
55
55
  /benchmarks/results/
56
56
  /benchmarks/__pycache__/
57
- .claude/
57
+ .claude/*
58
+ !.claude/skills/
58
59
  *.c
59
60
  *.so
60
61
  build/
@@ -63,3 +64,5 @@ demo/.env
63
64
  example/.env
64
65
  demo/data/*.db
65
66
  demo/logs/*.log
67
+ .claude/settings.local.json
68
+ .claude/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.8.2
3
+ Version: 3.8.7
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -41,6 +41,14 @@ Description-Content-Type: text/markdown
41
41
  Laravel joy. Python speed. 10x less code. Zero third-party dependencies.
42
42
  </p>
43
43
 
44
+ <p align="center">
45
+ <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">
48
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
49
+ <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
50
+ </p>
51
+
44
52
  <p align="center">
45
53
  <a href="https://tina4.com">Documentation</a> &bull;
46
54
  <a href="#getting-started">Getting Started</a> &bull;
@@ -49,15 +57,6 @@ Description-Content-Type: text/markdown
49
57
  <a href="https://tina4.com">tina4.com</a>
50
58
  </p>
51
59
 
52
- <p align="center">
53
- <img src="https://img.shields.io/badge/version-3.0.0-blue" alt="Version 3.0.0">
54
- <img src="https://img.shields.io/badge/tests-1633%20passing-brightgreen" alt="Tests">
55
- <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
56
- <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
57
- <img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+">
58
- <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
59
- </p>
60
-
61
60
  ---
62
61
 
63
62
  ## Quick Start
@@ -9,6 +9,14 @@
9
9
  Laravel joy. Python speed. 10x less code. Zero third-party dependencies.
10
10
  </p>
11
11
 
12
+ <p align="center">
13
+ <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">
16
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
+ <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
18
+ </p>
19
+
12
20
  <p align="center">
13
21
  <a href="https://tina4.com">Documentation</a> &bull;
14
22
  <a href="#getting-started">Getting Started</a> &bull;
@@ -17,15 +25,6 @@
17
25
  <a href="https://tina4.com">tina4.com</a>
18
26
  </p>
19
27
 
20
- <p align="center">
21
- <img src="https://img.shields.io/badge/version-3.0.0-blue" alt="Version 3.0.0">
22
- <img src="https://img.shields.io/badge/tests-1633%20passing-brightgreen" alt="Tests">
23
- <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
24
- <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
25
- <img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+">
26
- <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
27
- </p>
28
-
29
28
  ---
30
29
 
31
30
  ## Quick Start
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.8.2"
3
+ version = "3.8.7"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -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.8.2"
11
+ __version__ = "3.8.3"
12
12
 
13
13
  # ── HTTP Constants ──
14
14
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -644,6 +644,22 @@ async def app(scope: dict, receive, send):
644
644
  request_id = request.headers.get("x-request-id", str(uuid.uuid4())[:8])
645
645
  set_request_id(request_id)
646
646
 
647
+ # Auto-start session — lazy, reads cookie, saves on response
648
+ try:
649
+ from tina4_python.session import Session
650
+ cookie_header = dict(scope.get("headers", [])).get(b"cookie", b"").decode()
651
+ sid_match = None
652
+ for part in cookie_header.split(";"):
653
+ part = part.strip()
654
+ if part.startswith("tina4_session="):
655
+ sid_match = part.split("=", 1)[1]
656
+ break
657
+ sess = Session()
658
+ sess.start(sid_match)
659
+ request.session = sess
660
+ except Exception:
661
+ pass # Session module not available — session stays None
662
+
647
663
  response = Response()
648
664
  response.header("x-request-id", request_id)
649
665
 
@@ -764,17 +780,34 @@ async def app(scope: dict, receive, send):
764
780
  _sig = inspect.signature(route["handler"])
765
781
  _params = list(_sig.parameters.values())
766
782
  _pcount = len(_params)
767
- if _pcount == 0:
768
- result = await route["handler"]()
769
- elif _pcount == 1:
770
- # If type-hinted as Request, pass request; otherwise pass response
771
- _ann = _params[0].annotation
783
+
784
+ # Build args: inject path params by name, then request/response
785
+ _args = []
786
+ _remaining = []
787
+ for p in _params:
788
+ name = p.name
789
+ if name in params:
790
+ _args.append(params[name])
791
+ else:
792
+ _remaining.append(p)
793
+
794
+ # Append request/response for remaining positional params
795
+ if len(_remaining) == 0:
796
+ pass # All params were path params
797
+ elif len(_remaining) == 1:
798
+ _ann = _remaining[0].annotation
772
799
  if _ann is Request or (isinstance(_ann, str) and _ann in ("Request", "request")):
773
- result = await route["handler"](request)
800
+ _args.append(request)
774
801
  else:
775
- result = await route["handler"](response)
802
+ _args.append(response)
803
+ elif len(_remaining) >= 2:
804
+ _args.append(request)
805
+ _args.append(response)
806
+
807
+ if _pcount == 0:
808
+ result = await route["handler"]()
776
809
  else:
777
- result = await route["handler"](request, response)
810
+ result = await route["handler"](*_args)
778
811
  if isinstance(result, Response):
779
812
  response = result
780
813
  except Exception as e:
@@ -871,6 +904,17 @@ async def app(scope: dict, receive, send):
871
904
  # ETag check — 304 Not Modified
872
905
  if_none_match = request.headers.get("if-none-match", "")
873
906
 
907
+ # Save session and set cookie if session was used
908
+ if request.session is not None:
909
+ try:
910
+ request.session.save()
911
+ sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
912
+ if sid:
913
+ ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
914
+ response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age={ttl}")
915
+ except Exception:
916
+ pass
917
+
874
918
  # Build and send response
875
919
  accept_encoding = request.headers.get("accept-encoding", "")
876
920
  headers = response.build_headers(accept_encoding)
@@ -138,6 +138,19 @@ class ORM(metaclass=ORMMeta):
138
138
  data[db_col] = getattr(self, name)
139
139
  return data
140
140
 
141
+ @classmethod
142
+ def query(cls) -> "QueryBuilder":
143
+ """Create a fluent QueryBuilder pre-configured for this model's table and database.
144
+
145
+ Usage:
146
+ results = User.query().where("active = ?", [1]).order_by("name").get()
147
+
148
+ Returns:
149
+ A QueryBuilder instance bound to this model's table and database.
150
+ """
151
+ from tina4_python.query_builder import QueryBuilder
152
+ return QueryBuilder.from_table(cls._get_table(), cls._get_db())
153
+
141
154
  @classmethod
142
155
  def _get_table(cls) -> str:
143
156
  """Get table name — defaults to lowercase class name + 's'."""
@@ -0,0 +1,280 @@
1
+ # Tina4 QueryBuilder — Fluent SQL query builder.
2
+ """
3
+ Fluent SQL query builder for Tina4 Python.
4
+
5
+ Usage:
6
+ # Standalone
7
+ result = QueryBuilder.from_table("users", db) \\
8
+ .select("id", "name") \\
9
+ .where("active = ?", [1]) \\
10
+ .order_by("name ASC") \\
11
+ .limit(10) \\
12
+ .get()
13
+
14
+ # From ORM model
15
+ result = User.query() \\
16
+ .where("age > ?", [18]) \\
17
+ .order_by("name") \\
18
+ .get()
19
+ """
20
+
21
+
22
+ class QueryBuilder:
23
+ """Fluent SQL query builder that produces and executes SQL statements."""
24
+
25
+ def __init__(self, table: str, db=None):
26
+ """Private-ish constructor. Use from_table() or ORM.query()."""
27
+ self._table = table
28
+ self._db = db
29
+ self._columns: list[str] = ["*"]
30
+ self._wheres: list[tuple[str, str]] = []
31
+ self._params: list = []
32
+ self._joins: list[str] = []
33
+ self._group_by_cols: list[str] = []
34
+ self._havings: list[str] = []
35
+ self._having_params: list = []
36
+ self._order_by_cols: list[str] = []
37
+ self._limit_val: int | None = None
38
+ self._offset_val: int | None = None
39
+
40
+ @staticmethod
41
+ def from_table(table_name: str, db=None) -> "QueryBuilder":
42
+ """Create a QueryBuilder for a table.
43
+
44
+ Args:
45
+ table_name: The database table name.
46
+ db: Optional database connection (Database or adapter).
47
+
48
+ Returns:
49
+ A new QueryBuilder instance.
50
+ """
51
+ return QueryBuilder(table_name, db)
52
+
53
+ def select(self, *columns: str) -> "QueryBuilder":
54
+ """Set the columns to select.
55
+
56
+ Args:
57
+ *columns: Column names (default is '*').
58
+
59
+ Returns:
60
+ self for chaining.
61
+ """
62
+ if columns:
63
+ self._columns = list(columns)
64
+ return self
65
+
66
+ def where(self, condition: str, params: list = None) -> "QueryBuilder":
67
+ """Add a WHERE condition with AND.
68
+
69
+ Args:
70
+ condition: SQL condition with ? placeholders.
71
+ params: Parameter values for placeholders.
72
+
73
+ Returns:
74
+ self for chaining.
75
+ """
76
+ self._wheres.append(("AND", condition))
77
+ if params:
78
+ self._params.extend(params)
79
+ return self
80
+
81
+ def or_where(self, condition: str, params: list = None) -> "QueryBuilder":
82
+ """Add a WHERE condition with OR.
83
+
84
+ Args:
85
+ condition: SQL condition with ? placeholders.
86
+ params: Parameter values for placeholders.
87
+
88
+ Returns:
89
+ self for chaining.
90
+ """
91
+ self._wheres.append(("OR", condition))
92
+ if params:
93
+ self._params.extend(params)
94
+ return self
95
+
96
+ def join(self, table: str, on_clause: str) -> "QueryBuilder":
97
+ """Add an INNER JOIN.
98
+
99
+ Args:
100
+ table: Table to join.
101
+ on_clause: Join condition.
102
+
103
+ Returns:
104
+ self for chaining.
105
+ """
106
+ self._joins.append(f"INNER JOIN {table} ON {on_clause}")
107
+ return self
108
+
109
+ def left_join(self, table: str, on_clause: str) -> "QueryBuilder":
110
+ """Add a LEFT JOIN.
111
+
112
+ Args:
113
+ table: Table to join.
114
+ on_clause: Join condition.
115
+
116
+ Returns:
117
+ self for chaining.
118
+ """
119
+ self._joins.append(f"LEFT JOIN {table} ON {on_clause}")
120
+ return self
121
+
122
+ def group_by(self, column: str) -> "QueryBuilder":
123
+ """Add a GROUP BY column.
124
+
125
+ Args:
126
+ column: Column name to group by.
127
+
128
+ Returns:
129
+ self for chaining.
130
+ """
131
+ self._group_by_cols.append(column)
132
+ return self
133
+
134
+ def having(self, expression: str, params: list = None) -> "QueryBuilder":
135
+ """Add a HAVING clause.
136
+
137
+ Args:
138
+ expression: HAVING expression with ? placeholders.
139
+ params: Parameter values.
140
+
141
+ Returns:
142
+ self for chaining.
143
+ """
144
+ self._havings.append(expression)
145
+ if params:
146
+ self._having_params.extend(params)
147
+ return self
148
+
149
+ def order_by(self, expression: str) -> "QueryBuilder":
150
+ """Add an ORDER BY clause.
151
+
152
+ Args:
153
+ expression: Column and direction (e.g. "name ASC").
154
+
155
+ Returns:
156
+ self for chaining.
157
+ """
158
+ self._order_by_cols.append(expression)
159
+ return self
160
+
161
+ def limit(self, count: int, offset: int = None) -> "QueryBuilder":
162
+ """Set LIMIT and optional OFFSET.
163
+
164
+ Args:
165
+ count: Maximum rows to return.
166
+ offset: Number of rows to skip.
167
+
168
+ Returns:
169
+ self for chaining.
170
+ """
171
+ self._limit_val = count
172
+ if offset is not None:
173
+ self._offset_val = offset
174
+ return self
175
+
176
+ def to_sql(self) -> str:
177
+ """Build and return the SQL string without executing.
178
+
179
+ Returns:
180
+ The constructed SQL query string.
181
+ """
182
+ sql = f"SELECT {', '.join(self._columns)} FROM {self._table}"
183
+
184
+ if self._joins:
185
+ sql += " " + " ".join(self._joins)
186
+
187
+ if self._wheres:
188
+ sql += " WHERE " + self._build_where()
189
+
190
+ if self._group_by_cols:
191
+ sql += " GROUP BY " + ", ".join(self._group_by_cols)
192
+
193
+ if self._havings:
194
+ sql += " HAVING " + " AND ".join(self._havings)
195
+
196
+ if self._order_by_cols:
197
+ sql += " ORDER BY " + ", ".join(self._order_by_cols)
198
+
199
+ return sql
200
+
201
+ def get(self):
202
+ """Execute the query and return the DatabaseResult.
203
+
204
+ Returns:
205
+ DatabaseResult from db.fetch().
206
+ """
207
+ self._ensure_db()
208
+ sql = self.to_sql()
209
+ all_params = self._params + self._having_params
210
+
211
+ return self._db.fetch(
212
+ sql,
213
+ all_params or None,
214
+ self._limit_val if self._limit_val is not None else 100,
215
+ self._offset_val if self._offset_val is not None else 0,
216
+ )
217
+
218
+ def first(self) -> dict | None:
219
+ """Execute the query and return a single row.
220
+
221
+ Returns:
222
+ A dict for the first matching row, or None.
223
+ """
224
+ self._ensure_db()
225
+ sql = self.to_sql()
226
+ all_params = self._params + self._having_params
227
+
228
+ return self._db.fetch_one(sql, all_params or None)
229
+
230
+ def count(self) -> int:
231
+ """Execute the query and return the row count.
232
+
233
+ Returns:
234
+ Number of matching rows.
235
+ """
236
+ self._ensure_db()
237
+
238
+ # Build a count query by replacing columns
239
+ original = self._columns
240
+ self._columns = ["COUNT(*) as cnt"]
241
+ sql = self.to_sql()
242
+ self._columns = original
243
+
244
+ all_params = self._params + self._having_params
245
+
246
+ row = self._db.fetch_one(sql, all_params or None)
247
+ if row is None:
248
+ return 0
249
+ # Handle case-insensitive column names
250
+ return int(row.get("cnt", row.get("CNT", 0)))
251
+
252
+ def exists(self) -> bool:
253
+ """Check whether any matching rows exist.
254
+
255
+ Returns:
256
+ True if at least one row matches.
257
+ """
258
+ return self.count() > 0
259
+
260
+ # -- Private helpers --
261
+
262
+ def _build_where(self) -> str:
263
+ """Build the WHERE clause from accumulated conditions."""
264
+ parts = []
265
+ for i, (connector, condition) in enumerate(self._wheres):
266
+ if i == 0:
267
+ parts.append(condition)
268
+ else:
269
+ parts.append(f"{connector} {condition}")
270
+ return " ".join(parts)
271
+
272
+ def _ensure_db(self):
273
+ """Ensure a database connection is available."""
274
+ if self._db is None:
275
+ # Try to use the global ORM database
276
+ from tina4_python.orm.model import _database
277
+ if _database is not None:
278
+ self._db = _database
279
+ else:
280
+ raise RuntimeError("QueryBuilder: No database connection provided.")
@@ -208,11 +208,14 @@ class Session:
208
208
  self._data[key] = value
209
209
  self._dirty = True
210
210
 
211
- def unset(self, key: str):
211
+ def delete(self, key: str):
212
212
  """Remove a session key."""
213
213
  self._data.pop(key, None)
214
214
  self._dirty = True
215
215
 
216
+ # Alias for backward compatibility
217
+ unset = delete
218
+
216
219
  def has(self, key: str) -> bool:
217
220
  return key in self._data
218
221