tina4-python 3.12.13__tar.gz → 3.13.0__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 (157) hide show
  1. {tina4_python-3.12.13 → tina4_python-3.13.0}/PKG-INFO +3 -3
  2. {tina4_python-3.12.13 → tina4_python-3.13.0}/README.md +2 -2
  3. {tina4_python-3.12.13 → tina4_python-3.13.0}/pyproject.toml +1 -1
  4. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/CLAUDE.md +4 -4
  5. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/__init__.py +24 -1
  6. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/ai/__init__.py +74 -7
  7. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/api/__init__.py +46 -4
  8. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/auth/__init__.py +43 -15
  9. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/cli/__init__.py +12 -6
  10. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/container/__init__.py +21 -2
  11. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/response.py +70 -17
  12. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/router.py +85 -1
  13. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/adapter.py +17 -0
  14. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/connection.py +43 -0
  15. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/frond/engine.py +54 -16
  16. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/graphql/__init__.py +91 -0
  17. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/i18n/__init__.py +42 -5
  18. tina4_python-3.13.0/tina4_python/migration/__init__.py +47 -0
  19. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/orm/model.py +58 -9
  20. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/__init__.py +55 -11
  21. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/job.py +6 -1
  22. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/session/__init__.py +31 -0
  23. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/swagger/__init__.py +76 -13
  24. tina4_python-3.13.0/tina4_python/test/__init__.py +431 -0
  25. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/websocket/__init__.py +16 -0
  26. tina4_python-3.12.13/tina4_python/migration/__init__.py +0 -15
  27. {tina4_python-3.12.13 → tina4_python-3.13.0}/.gitignore +0 -0
  28. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/HtmlElement.py +0 -0
  29. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/Testing.py +0 -0
  30. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/cache/__init__.py +0 -0
  31. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/__init__.py +0 -0
  32. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/cache.py +0 -0
  33. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/constants.py +0 -0
  34. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/events.py +0 -0
  35. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/middleware.py +0 -0
  36. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/rate_limiter.py +0 -0
  37. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/request.py +0 -0
  38. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/core/server.py +0 -0
  39. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/crud/__init__.py +0 -0
  40. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/__init__.py +0 -0
  41. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/firebird.py +0 -0
  42. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/mongodb.py +0 -0
  43. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/mssql.py +0 -0
  44. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/mysql.py +0 -0
  45. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/odbc.py +0 -0
  46. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/postgres.py +0 -0
  47. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/database/sqlite.py +0 -0
  48. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/debug/__init__.py +0 -0
  49. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/debug/error_overlay.py +0 -0
  50. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/dev_admin/__init__.py +0 -0
  51. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/dev_admin/metrics.py +0 -0
  52. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/dev_admin/plan.py +0 -0
  53. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/dev_admin/project_index.py +0 -0
  54. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/docs.py +0 -0
  55. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/dotenv/__init__.py +0 -0
  56. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/frond/FROND.md +0 -0
  57. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/frond/__init__.py +0 -0
  58. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/auth/meta.json +0 -0
  59. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  60. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/database/meta.json +0 -0
  61. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  62. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
  63. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  64. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/orm/meta.json +0 -0
  65. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  66. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  67. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/queue/meta.json +0 -0
  68. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  69. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/rest-api/meta.json +0 -0
  70. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  71. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/templates/meta.json +0 -0
  72. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  73. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  74. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/mcp/__init__.py +0 -0
  75. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/mcp/protocol.py +0 -0
  76. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/mcp/tools.py +0 -0
  77. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/messenger/__init__.py +0 -0
  78. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/migration/runner.py +0 -0
  79. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/orm/__init__.py +0 -0
  80. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/orm/fields.py +0 -0
  81. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/__feedback/widget.js +0 -0
  82. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/css/tina4.css +0 -0
  83. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/css/tina4.min.css +0 -0
  84. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/favicon.ico +0 -0
  85. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/images/logo.svg +0 -0
  86. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  87. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/frond.js +0 -0
  88. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/frond.min.js +0 -0
  89. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  90. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  91. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/tina4.min.js +0 -0
  92. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/js/tina4js.min.js +0 -0
  93. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/swagger/index.html +0 -0
  94. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  95. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/query_builder/__init__.py +0 -0
  96. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/kafka_backend.py +0 -0
  97. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/lite_backend.py +0 -0
  98. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/mongo_backend.py +0 -0
  99. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue_backends/__init__.py +0 -0
  101. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
  102. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
  103. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  104. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/__init__.py +0 -0
  105. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  106. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
  107. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  108. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
  109. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
  110. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
  111. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
  112. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
  113. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
  114. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
  115. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
  116. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  117. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
  118. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/base.scss +0 -0
  119. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/colors.scss +0 -0
  120. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
  121. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/seeder/__init__.py +0 -0
  122. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/service/__init__.py +0 -0
  123. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/session_handlers/__init__.py +0 -0
  124. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  125. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/session_handlers/redis_handler.py +0 -0
  126. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
  127. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/components/crud.twig +0 -0
  128. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  129. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  130. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
  131. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  132. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/302.twig +0 -0
  133. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/401.twig +0 -0
  134. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/403.twig +0 -0
  135. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/404.twig +0 -0
  136. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/500.twig +0 -0
  137. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/502.twig +0 -0
  138. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/503.twig +0 -0
  139. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/errors/base.twig +0 -0
  140. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/frontend/README.md +0 -0
  141. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/templates/readme.md +0 -0
  142. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/test_client/__init__.py +0 -0
  143. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/validator/__init__.py +0 -0
  156. {tina4_python-3.12.13 → tina4_python-3.13.0}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.12.13 → tina4_python-3.13.0}/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.12.13
3
+ Version: 3.13.0
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
@@ -452,7 +452,7 @@ request.session.set("user_id", 42)
452
452
  user_id = request.session.get("user_id")
453
453
  ```
454
454
 
455
- Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_HANDLER` in `.env`.
455
+ Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_BACKEND` in `.env`.
456
456
 
457
457
  ### Queues
458
458
 
@@ -699,7 +699,7 @@ TINA4_DATABASE_URL=sqlite:///data/app.db
699
699
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
700
700
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
701
701
  TINA4_LOCALE=en # en, fr, af, zh, ja, es
702
- TINA4_SESSION_HANDLER=SessionFileHandler
702
+ TINA4_SESSION_BACKEND=SessionFileHandler
703
703
  SWAGGER_TITLE=My API
704
704
  ```
705
705
 
@@ -420,7 +420,7 @@ request.session.set("user_id", 42)
420
420
  user_id = request.session.get("user_id")
421
421
  ```
422
422
 
423
- Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_HANDLER` in `.env`.
423
+ Backends: file (default), Redis, Valkey, MongoDB, database. Set via `TINA4_SESSION_BACKEND` in `.env`.
424
424
 
425
425
  ### Queues
426
426
 
@@ -667,7 +667,7 @@ TINA4_DATABASE_URL=sqlite:///data/app.db
667
667
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
668
668
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
669
669
  TINA4_LOCALE=en # en, fr, af, zh, ja, es
670
- TINA4_SESSION_HANDLER=SessionFileHandler
670
+ TINA4_SESSION_BACKEND=SessionFileHandler
671
671
  SWAGGER_TITLE=My API
672
672
  ```
673
673
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.12.13"
3
+ version = "3.13.0"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -498,7 +498,7 @@ TINA4_TOKEN_LIMIT is used to set the session time, default 60 minutes
498
498
 
499
499
  ### Session Backends
500
500
 
501
- Set `TINA4_SESSION_HANDLER` to choose a backend:
501
+ Set `TINA4_SESSION_BACKEND` to choose a backend:
502
502
 
503
503
  | Handler | Backend | Required package |
504
504
  |---------|---------|-----------------|
@@ -510,7 +510,7 @@ Set `TINA4_SESSION_HANDLER` to choose a backend:
510
510
  #### MongoDB session env vars
511
511
 
512
512
  ```bash
513
- TINA4_SESSION_HANDLER=SessionMongoHandler
513
+ TINA4_SESSION_BACKEND=SessionMongoHandler
514
514
  TINA4_SESSION_MONGO_HOST=localhost # default
515
515
  TINA4_SESSION_MONGO_PORT=27017 # default
516
516
  TINA4_SESSION_MONGO_URI= # full URI (overrides host/port)
@@ -1411,7 +1411,7 @@ async def products(request, response):
1411
1411
  return response(expensive_query())
1412
1412
 
1413
1413
  # Per-route TTL override via @cached decorator
1414
- @cached(True, max_age=120)
1414
+ @cached(max_age=120)
1415
1415
  @get("/api/slow")
1416
1416
  async def slow(request, response):
1417
1417
  return response(very_slow_query())
@@ -1578,7 +1578,7 @@ TINA4_OVERRIDE_CLIENT=false # Set to true to allow running without tina4 C
1578
1578
  HOST_NAME=localhost:7145
1579
1579
 
1580
1580
  # Sessions
1581
- TINA4_SESSION_HANDLER=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
1581
+ TINA4_SESSION_BACKEND=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
1582
1582
  TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
1583
1583
 
1584
1584
  # Swagger/OpenAPI
@@ -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.12.13"
11
+ __version__ = "3.13.0"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -61,3 +61,26 @@ from tina4_python.container import Container # noqa: E402, F401
61
61
 
62
62
  # ── Server ──
63
63
  from tina4_python.core.server import run, background # noqa: E402, F401
64
+
65
+ # ── HTTP Client ──
66
+ from tina4_python.api import Api # noqa: E402, F401
67
+
68
+ # ── SOAP / WSDL ──
69
+ from tina4_python.wsdl import WSDL, wsdl_operation # noqa: E402, F401
70
+
71
+ # ── GraphQL ──
72
+ from tina4_python.graphql import GraphQL # noqa: E402, F401
73
+
74
+ # ── Auto-CRUD scaffolder ──
75
+ from tina4_python.crud import AutoCrud # noqa: E402, F401
76
+
77
+ # ── Events (decoupled communication) ──
78
+ from tina4_python.core.events import on, emit, once, off # noqa: E402, F401
79
+
80
+ # ── Email (Messenger) ──
81
+ from tina4_python.messenger import Messenger # noqa: E402, F401
82
+
83
+ # ── Inline testing (@tests + assertions for inline test cases) ──
84
+ # Class-based xUnit testing lives in tina4_python.test (a separate module).
85
+ # Keep both surfaces re-exported so users can write either style.
86
+ from tina4_python.Testing import tests, assert_equal as assert_equal_inline # noqa: E402, F401
@@ -1,9 +1,14 @@
1
- # Tina4 AI — Install AI coding assistant context files.
1
+ # Tina4 AI — Detect and install AI coding assistant context files.
2
2
  """
3
- Simple menu-driven installer for AI tool context files.
4
- The user picks which tools they use, we install the appropriate files.
3
+ Detect which AI tools are present in a project and install Tina4-aware
4
+ context files so any assistant understands the framework.
5
5
 
6
- from tina4_python.ai import show_menu, install_selected
6
+ from tina4_python.ai import detect_ai, install_context, status_report
7
+
8
+ tools = detect_ai(".") # → [{"name": "claude-code", "installed": True, ...}, ...]
9
+ install_context(".") # install for all known tools
10
+ install_context(".", tools=["claude-code", "cursor"])
11
+ print(status_report(".")) # human-readable summary
7
12
  """
8
13
  import os
9
14
  import shutil
@@ -29,6 +34,47 @@ def is_installed(root: str, tool: dict) -> bool:
29
34
  return (Path(root).resolve() / tool["context_file"]).exists()
30
35
 
31
36
 
37
+ # ── Detection API (docs-friendly names) ────────────────────────────────────
38
+
39
+
40
+ def detect_ai(root: str = ".") -> list[dict]:
41
+ """Detect which AI coding tools are installed in ``root``.
42
+
43
+ Returns a list of tool dicts, one per known AI tool, with an
44
+ ``installed`` boolean added. The list mirrors ``AI_TOOLS`` order.
45
+
46
+ from tina4_python.ai import detect_ai
47
+ tools = detect_ai(".")
48
+ installed = [t for t in tools if t["installed"]]
49
+ """
50
+ return [{**t, "installed": is_installed(root, t)} for t in AI_TOOLS]
51
+
52
+
53
+ def detect_ai_names(root: str = ".") -> list[str]:
54
+ """Return only the names of detected (installed) AI tools.
55
+
56
+ names = detect_ai_names(".") # → ["claude-code", "cursor"]
57
+ """
58
+ return [t["name"] for t in AI_TOOLS if is_installed(root, t)]
59
+
60
+
61
+ def status_report(root: str = ".") -> str:
62
+ """Human-readable summary of AI-tool installation state in ``root``.
63
+
64
+ Returns a multi-line string suitable for printing — lists each tool
65
+ along with installed/not-installed and the path of the context file.
66
+ """
67
+ root = str(Path(root).resolve())
68
+ lines = [f"AI tool status — {root}", ""]
69
+ for tool in AI_TOOLS:
70
+ marker = "✓ installed" if is_installed(root, tool) else " not present"
71
+ lines.append(f" {marker} {tool['description']:<24s} → {tool['context_file']}")
72
+ installed_count = sum(1 for t in AI_TOOLS if is_installed(root, t))
73
+ lines.append("")
74
+ lines.append(f" {installed_count}/{len(AI_TOOLS)} tools have Tina4 context.")
75
+ return "\n".join(lines)
76
+
77
+
32
78
  def show_menu(root: str = ".") -> str:
33
79
  """Print the numbered menu and return user input."""
34
80
  root = str(Path(root).resolve())
@@ -89,9 +135,30 @@ def install_selected(root: str, selection: str) -> list[str]:
89
135
  return created
90
136
 
91
137
 
92
- def install_all(root: str = ".") -> list[str]:
93
- """Install context for all AI tools (non-interactive)."""
94
- return install_selected(root, "all")
138
+ def install_context(root: str = ".", tools: "list[str] | None" = None) -> list[str]:
139
+ """Install Tina4 context files for AI tools (non-interactive).
140
+
141
+ Args:
142
+ root: Project root directory.
143
+ tools: Optional list of tool names. When omitted, installs for all
144
+ known AI tools.
145
+
146
+ Returns a list of relative paths to the files that were created.
147
+
148
+ install_context(".") # all tools
149
+ install_context(".", tools=["claude-code"]) # just one
150
+ """
151
+ if tools is None:
152
+ selection = "all"
153
+ else:
154
+ # Translate names → indices for install_selected's CSV input
155
+ index_map = {t["name"]: str(i + 1) for i, t in enumerate(AI_TOOLS)}
156
+ try:
157
+ selection = ",".join(index_map[name] for name in tools)
158
+ except KeyError as e:
159
+ known = ", ".join(t["name"] for t in AI_TOOLS)
160
+ raise ValueError(f"Unknown AI tool: {e!s}. Known: {known}") from e
161
+ return install_selected(root, selection)
95
162
 
96
163
 
97
164
  def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
@@ -20,13 +20,50 @@ class Api:
20
20
  """HTTP client using urllib — zero external dependencies."""
21
21
 
22
22
  def __init__(self, base_url: str = "", auth_header: str = "",
23
- ignore_ssl: bool = False, timeout: int = 30):
23
+ ignore_ssl: bool = False, timeout: int = 30,
24
+ bearer_token: str | None = None,
25
+ username: str | None = None,
26
+ password: str | None = None,
27
+ headers: dict[str, str] | None = None,
28
+ verify_ssl: bool | None = None):
29
+ """HTTP client.
30
+
31
+ Constructor accepts ergonomic kwargs the documentation has long
32
+ described — every modern Python HTTP library (requests, httpx)
33
+ accepts these directly rather than requiring post-construction
34
+ setter calls.
35
+
36
+ api = Api("https://api.example.com", bearer_token="sk-...")
37
+ api = Api("https://api.example.com", username="u", password="p")
38
+ api = Api("https://api.example.com", headers={"X-Tenant": "acme"})
39
+ api = Api("https://self-signed.local", verify_ssl=False)
40
+
41
+ The setter-based API (``set_bearer_token``, ``set_basic_auth``,
42
+ ``add_headers``) continues to work; pick whichever reads better.
43
+
44
+ ``verify_ssl`` is the docs-friendly inverse of ``ignore_ssl`` —
45
+ ``verify_ssl=False`` is equivalent to ``ignore_ssl=True``. If
46
+ both are supplied, ``ignore_ssl`` wins (legacy precedence).
47
+ """
24
48
  self.base_url = base_url.rstrip("/")
25
49
  self.auth_header = auth_header
26
50
  self.timeout = timeout
27
51
  self._headers: dict[str, str] = {}
28
52
  self._ssl_context = None
29
- if ignore_ssl:
53
+
54
+ # ── kwarg sugar ────────────────────────────────────────────────
55
+ # Bearer token wins over basic auth if both are passed.
56
+ if bearer_token is not None:
57
+ self.set_bearer_token(bearer_token)
58
+ elif username is not None and password is not None:
59
+ self.set_basic_auth(username, password)
60
+
61
+ if headers:
62
+ self._headers.update(headers)
63
+
64
+ # ignore_ssl is the existing flag; verify_ssl=False is the same thing
65
+ # expressed positively. Honour ignore_ssl when both are set.
66
+ if ignore_ssl or (verify_ssl is False):
30
67
  self._ssl_context = ssl.create_default_context()
31
68
  self._ssl_context.check_hostname = False
32
69
  self._ssl_context.verify_mode = ssl.CERT_NONE
@@ -68,9 +105,14 @@ class Api:
68
105
  """HTTP DELETE request."""
69
106
  return self._request("DELETE", self._url(path), body)
70
107
 
71
- def send_request(self, method: str, path: str = "", body=None,
108
+ def send(self, method: str, path: str = "", body=None,
72
109
  content_type: str = "application/json") -> dict:
73
- """Generic request method."""
110
+ """Generic request method — pick HTTP verb at call time.
111
+
112
+ Renamed from ``send_request`` in 3.13.0 for parity with the
113
+ documentation and conciseness (``api.send("PATCH", ...)`` reads
114
+ cleaner than ``api.send_request("PATCH", ...)``).
115
+ """
74
116
  return self._request(method.upper(), self._url(path), body, content_type)
75
117
 
76
118
  def _url(self, path: str) -> str:
@@ -62,8 +62,14 @@ class Auth:
62
62
  """
63
63
  self.secret = secret or os.environ.get("TINA4_SECRET", "tina4-default-secret")
64
64
  self.algorithm = algorithm
65
+ # JWT expiry env var:
66
+ # - TINA4_TOKEN_EXPIRES_IN (preferred — matches docs and the
67
+ # form-token expiry env var used by Frond)
68
+ # - TINA4_TOKEN_LIMIT (legacy — accepted for backward compat)
69
+ # Constructor arg wins over both env vars.
65
70
  self.expires_in = expires_in or int(
66
- os.environ.get("TINA4_TOKEN_LIMIT", "60")
71
+ os.environ.get("TINA4_TOKEN_EXPIRES_IN")
72
+ or os.environ.get("TINA4_TOKEN_LIMIT", "60")
67
73
  )
68
74
 
69
75
  # ── JWT ────────────────────────────────────────────────────────
@@ -98,26 +104,42 @@ class Auth:
98
104
  return f"{h}.{p}.{signature}"
99
105
 
100
106
  @_DualMethod
101
- def valid_token(self, token: str) -> bool:
102
- """Validate a JWT signature and expiry. Returns True if valid, False otherwise."""
107
+ def valid_token(self, token: str) -> "dict | None":
108
+ """Validate a JWT signature + expiry. Returns the decoded payload
109
+ on success, or ``None`` if invalid / expired / malformed.
110
+
111
+ The payload dict is truthy for valid tokens and ``None`` is falsy,
112
+ so the legacy ``if Auth.valid_token(t):`` boolean-style usage keeps
113
+ working unchanged. Callers that want the payload can now read it
114
+ directly instead of a separate ``get_payload(t)`` call::
115
+
116
+ payload = Auth.valid_token(token)
117
+ if payload is None:
118
+ return response("Unauthorized", 401)
119
+ user_id = payload["user_id"]
120
+
121
+ Matches the convention used by PyJWT, python-jose, and authlib —
122
+ validity check returns the payload because the validation IS the
123
+ payload decode. Returning bare True/False discarded that information.
124
+ """
103
125
  try:
104
126
  parts = token.split(".")
105
127
  if len(parts) != 3:
106
- return False
128
+ return None
107
129
 
108
130
  h, p, sig = parts
109
131
  expected = self._sign(f"{h}.{p}")
110
132
  if not hmac.compare_digest(sig, expected):
111
- return False
133
+ return None
112
134
 
113
135
  payload = json.loads(_b64url_decode(p))
114
136
 
115
137
  if "exp" in payload and time.time() > payload["exp"]:
116
- return False
138
+ return None
117
139
 
118
- return True
140
+ return payload
119
141
  except Exception:
120
- return False
142
+ return None
121
143
 
122
144
  @_DualMethod
123
145
  def get_payload(self, token: str) -> dict | None:
@@ -137,9 +159,7 @@ class Auth:
137
159
  Args:
138
160
  expires_in: Lifetime in minutes (default: self.expires_in).
139
161
  """
140
- if not self.valid_token(token):
141
- return None
142
- payload = self.get_payload(token)
162
+ payload = self.valid_token(token)
143
163
  if payload is None:
144
164
  return None
145
165
  payload.pop("iat", None)
@@ -166,8 +186,11 @@ class Auth:
166
186
  return auth.get_token(payload)
167
187
 
168
188
  @classmethod
169
- def valid_token_static(cls, token: str) -> bool:
170
- """Validate a JWT without instantiating Auth — reads SECRET from env."""
189
+ def valid_token_static(cls, token: str) -> "dict | None":
190
+ """Validate a JWT without instantiating Auth — reads SECRET from env.
191
+
192
+ Returns the decoded payload on success, ``None`` on failure.
193
+ """
171
194
  secret = os.environ.get("TINA4_SECRET", "tina4-default-secret")
172
195
  auth = cls(secret=secret)
173
196
  return auth.valid_token(token)
@@ -307,8 +330,13 @@ def get_token(payload: dict, expires_in: int = 60, secret: str = None) -> str:
307
330
  return Auth.get_token_static(payload, expires_in=expires_in)
308
331
 
309
332
 
310
- def valid_token(token: str) -> bool:
311
- """Validate a JWT signature and expiry — reads SECRET from env. Returns True if valid."""
333
+ def valid_token(token: str) -> "dict | None":
334
+ """Validate a JWT signature and expiry — reads SECRET from env.
335
+
336
+ Returns the decoded payload dict on success, ``None`` on failure.
337
+ The truthy/falsy split keeps legacy ``if valid_token(t):`` code working
338
+ while letting new code read the payload directly.
339
+ """
312
340
  return Auth.valid_token_static(token)
313
341
 
314
342
 
@@ -399,9 +399,9 @@ def _init(args):
399
399
  )
400
400
 
401
401
  # AI context
402
- from tina4_python.ai import install_all
402
+ from tina4_python.ai import install_context
403
403
  if "--ai" in args:
404
- created = install_all(str(target))
404
+ created = install_context(str(target))
405
405
  if created:
406
406
  print("\nAI context installed for all supported tools:")
407
407
  for f in created:
@@ -602,10 +602,10 @@ def _build(args):
602
602
 
603
603
  def _ai(args):
604
604
  """Install AI coding assistant context files."""
605
- from tina4_python.ai import show_menu, install_selected, install_all
605
+ from tina4_python.ai import show_menu, install_selected, install_context
606
606
 
607
607
  if args and args[0].lower() == "all":
608
- install_all(".")
608
+ install_context(".")
609
609
  else:
610
610
  selection = show_menu(".")
611
611
  if selection:
@@ -743,8 +743,14 @@ async def list_{route_path}(request, response):
743
743
  page = int(request.params.get("page", 1))
744
744
  per_page = int(request.params.get("per_page", 20))
745
745
  offset = (page - 1) * per_page
746
- results = {model}().select(limit=per_page, offset=offset)
747
- return response(results.to_paginate(page=page, per_page=per_page))
746
+ records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
747
+ return response({{
748
+ "records": [r.to_dict() for r in records],
749
+ "count": total,
750
+ "page": page,
751
+ "per_page": per_page,
752
+ "total_pages": max(1, -(-total // per_page)),
753
+ }})
748
754
 
749
755
 
750
756
  @noauth()
@@ -27,7 +27,8 @@ class Container:
27
27
  subsequent calls return the memoised instance.
28
28
  - ``get(name)`` — resolve a dependency by name.
29
29
  - ``has(name)`` — check if a name is registered.
30
- - ``reset()`` — clear all registrations.
30
+ - ``reset()`` — clear cached singletons; keep factories.
31
+ - ``reset_all()`` — clear both singletons AND factories.
31
32
  """
32
33
 
33
34
  def __init__(self):
@@ -80,6 +81,24 @@ class Container:
80
81
  return name in self._factories
81
82
 
82
83
  def reset(self) -> None:
83
- """Clear all registrations."""
84
+ """Clear cached singleton instances; keep factory registrations.
85
+
86
+ Common test-fixture pattern: reset to drop cached state between
87
+ tests while keeping the DI graph wired. Transient registrations
88
+ are unaffected (they have no cache to clear).
89
+
90
+ For the old behaviour (clear everything including factories) use
91
+ ``reset_all()``.
92
+ """
93
+ with self._lock:
94
+ for entry in self._factories.values():
95
+ if entry.get("singleton"):
96
+ entry["instance"] = None
97
+
98
+ def reset_all(self) -> None:
99
+ """Clear ALL registrations — both factories and cached singletons.
100
+
101
+ Tear-down for end-of-suite / process-shutdown scenarios.
102
+ """
84
103
  with self._lock:
85
104
  self._factories.clear()
@@ -87,18 +87,26 @@ class Response:
87
87
  self._is_streaming: bool = False
88
88
  self._stream_source = None
89
89
 
90
- def __call__(self, data=None, status_code: int = 200, content_type: str = None) -> "Response":
90
+ def __call__(self, data=None, status_code: int = 200, content_type: str = None,
91
+ headers: dict | None = None) -> "Response":
91
92
  """Smart callable — auto-detects content type from data.
92
93
 
93
94
  Usage:
94
- return response({"key": "value"}) # JSON
95
- return response({"ok": True}, HTTP_CREATED) # JSON with status
96
- return response("<h1>Hello</h1>") # HTML
97
- return response("plain text", HTTP_OK) # Plain text
98
- return response(data, HTTP_OK, APPLICATION_JSON) # Explicit
95
+ return response({"key": "value"}) # JSON
96
+ return response({"ok": True}, HTTP_CREATED) # JSON with status
97
+ return response("<h1>Hello</h1>") # HTML
98
+ return response("plain text", HTTP_OK) # Plain text
99
+ return response(data, HTTP_OK, APPLICATION_JSON) # Explicit
100
+ return response(data, headers={"X-Tenant": "acme"}) # One-shot headers
99
101
  """
100
102
  self.status_code = status_code
101
103
 
104
+ # Optional one-shot headers — equivalent to chaining .header(k, v)
105
+ # for each entry, but lets call sites stay on a single expression.
106
+ if headers:
107
+ for k, v in headers.items():
108
+ self._headers.append((k, v))
109
+
102
110
  if content_type:
103
111
  # Explicit content type provided
104
112
  self.content_type = content_type
@@ -149,15 +157,48 @@ class Response:
149
157
  """Add a response header (chainable). Alias for header()."""
150
158
  return self.header(name, value)
151
159
 
152
- def cookie(self, name: str, value: str, path: str = "/",
153
- max_age: int = 3600, http_only: bool = True,
154
- secure: bool = False, same_site: str = "Lax") -> "Response":
155
- """Set a cookie (chainable)."""
156
- parts = [f"{name}={value}", f"Path={path}", f"Max-Age={max_age}",
157
- f"SameSite={same_site}"]
158
- if http_only:
160
+ def cookie(self, name: str, value: str, options=None, *,
161
+ path: str = None, max_age: int = None, http_only: bool = None,
162
+ secure: bool = None, same_site: str = None) -> "Response":
163
+ """Set a cookie (chainable). Two equivalent forms:
164
+
165
+ # Kwarg form (original)
166
+ response.cookie("session", token, max_age=3600, http_only=True)
167
+
168
+ # Dict-options form (when config comes from a settings object)
169
+ COOKIE_OPTS = {"max_age": 3600, "http_only": True, "secure": True}
170
+ response.cookie("session", token, COOKIE_OPTS)
171
+
172
+ When ``options`` is a dict, its values become the defaults; any
173
+ explicit kwarg passed afterwards overrides individual entries.
174
+ """
175
+ # Defaults
176
+ _path = "/"
177
+ _max_age = 3600
178
+ _http_only = True
179
+ _secure = False
180
+ _same_site = "Lax"
181
+
182
+ # Dict-options form
183
+ if isinstance(options, dict):
184
+ _path = options.get("path", _path)
185
+ _max_age = options.get("max_age", _max_age)
186
+ _http_only = options.get("http_only", _http_only)
187
+ _secure = options.get("secure", _secure)
188
+ _same_site = options.get("same_site", _same_site)
189
+
190
+ # Explicit kwargs win over dict
191
+ if path is not None: _path = path
192
+ if max_age is not None: _max_age = max_age
193
+ if http_only is not None: _http_only = http_only
194
+ if secure is not None: _secure = secure
195
+ if same_site is not None: _same_site = same_site
196
+
197
+ parts = [f"{name}={value}", f"Path={_path}", f"Max-Age={_max_age}",
198
+ f"SameSite={_same_site}"]
199
+ if _http_only:
159
200
  parts.append("HttpOnly")
160
- if secure:
201
+ if _secure:
161
202
  parts.append("Secure")
162
203
  self._cookies.append("; ".join(parts))
163
204
  return self
@@ -252,19 +293,28 @@ class Response:
252
293
  )
253
294
  return self
254
295
 
255
- def render(self, template: str, data: dict = None) -> "Response":
296
+ def render(self, template: str, data: dict = None, status_code: int = None) -> "Response":
256
297
  """Render a Frond/Twig template with data.
257
298
 
258
299
  Uses the global Frond engine (registered via set_frond()) so that
259
300
  custom filters and globals are available in all templates.
260
301
  Falls back to framework templates if not found in user dir.
302
+
303
+ The optional ``status_code`` lets error-page handlers render the
304
+ page and set the response status in one call::
305
+
306
+ return response.render("errors/404.twig", {}, 404)
307
+ return response.render("errors/500.twig", {"err": str(e)}, 500)
261
308
  """
262
309
  engine = get_frond()
263
310
 
264
311
  # Try user templates first (the global engine's directory)
265
312
  try:
266
313
  html = engine.render(template, data or {})
267
- return self.html(html)
314
+ rendered = self.html(html)
315
+ if status_code is not None:
316
+ rendered.status_code = status_code
317
+ return rendered
268
318
  except FileNotFoundError:
269
319
  pass
270
320
  except Exception as e:
@@ -275,7 +325,10 @@ class Response:
275
325
  if fw_engine is not None:
276
326
  try:
277
327
  html = fw_engine.render(template, data or {})
278
- return self.html(html)
328
+ rendered = self.html(html)
329
+ if status_code is not None:
330
+ rendered.status_code = status_code
331
+ return rendered
279
332
  except FileNotFoundError:
280
333
  pass
281
334
  except Exception as e: