tina4-python 3.10.97__tar.gz → 3.11.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 (151) hide show
  1. {tina4_python-3.10.97 → tina4_python-3.11.0}/.gitignore +11 -0
  2. {tina4_python-3.10.97 → tina4_python-3.11.0}/PKG-INFO +23 -1
  3. {tina4_python-3.10.97 → tina4_python-3.11.0}/README.md +22 -0
  4. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/CLAUDE.md +2 -0
  5. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/__init__.py +2 -2
  6. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/server.py +56 -0
  7. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/engine.py +49 -6
  8. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/model.py +17 -12
  9. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/__init__.py +7 -2
  10. {tina4_python-3.10.97 → tina4_python-3.11.0}/pyproject.toml +0 -0
  11. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/ai/__init__.py +0 -0
  14. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/cli/__init__.py +0 -0
  18. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/rate_limiter.py +0 -0
  25. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_reload.py +0 -0
  44. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/auth/meta.json +0 -0
  48. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  49. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/database/meta.json +0 -0
  50. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  51. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
  52. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  53. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/meta.json +0 -0
  54. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  55. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  56. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/queue/meta.json +0 -0
  57. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  58. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/rest-api/meta.json +0 -0
  59. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  60. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/meta.json +0 -0
  61. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  62. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  63. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/graphql/__init__.py +0 -0
  64. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/i18n/__init__.py +0 -0
  65. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/__init__.py +0 -0
  66. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/protocol.py +0 -0
  67. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/tools.py +0 -0
  68. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/messenger/__init__.py +0 -0
  69. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/migration/__init__.py +0 -0
  70. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/migration/runner.py +0 -0
  71. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/__init__.py +0 -0
  72. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/fields.py +0 -0
  73. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/css/tina4.css +0 -0
  74. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/css/tina4.min.css +0 -0
  75. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/favicon.ico +0 -0
  76. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/images/logo.svg +0 -0
  77. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  78. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/frond.min.js +0 -0
  79. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  80. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  81. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4.min.js +0 -0
  82. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4js.min.js +0 -0
  83. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/swagger/index.html +0 -0
  84. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  85. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/query_builder/__init__.py +0 -0
  86. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/__init__.py +0 -0
  87. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/job.py +0 -0
  88. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/kafka_backend.py +0 -0
  89. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/lite_backend.py +0 -0
  90. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/mongo_backend.py +0 -0
  91. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/rabbitmq_backend.py +0 -0
  92. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/__init__.py +0 -0
  93. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
  94. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
  95. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  97. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
  98. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  99. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
  100. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
  101. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
  102. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
  103. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
  104. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
  105. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
  106. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
  107. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  108. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
  109. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/base.scss +0 -0
  110. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/colors.scss +0 -0
  111. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
  112. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/seeder/__init__.py +0 -0
  113. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/service/__init__.py +0 -0
  114. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session/__init__.py +0 -0
  115. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/__init__.py +0 -0
  116. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  117. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/redis_handler.py +0 -0
  118. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
  119. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/swagger/__init__.py +0 -0
  120. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/components/crud.twig +0 -0
  121. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  122. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  123. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
  124. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  125. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/302.twig +0 -0
  126. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/401.twig +0 -0
  127. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/403.twig +0 -0
  128. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/404.twig +0 -0
  129. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/500.twig +0 -0
  130. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/502.twig +0 -0
  131. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/503.twig +0 -0
  132. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/base.twig +0 -0
  133. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/frontend/README.md +0 -0
  134. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/readme.md +0 -0
  135. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/test_client/__init__.py +0 -0
  136. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  137. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  138. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  141. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  142. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/validator/__init__.py +0 -0
  149. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/websocket/__init__.py +0 -0
  150. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/websocket/backplane.py +0 -0
  151. {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/wsdl/__init__.py +0 -0
@@ -61,5 +61,16 @@ __pycache__/
61
61
  build/
62
62
  .pytest_cache/
63
63
  example/.env
64
+ example/data/*.db
65
+ example/data/*.db-*
66
+ example/store/data/*.db
67
+ example/store/data/*.db-*
68
+ example/store/data/broken/
69
+ example/store/data/.broken/
70
+ example/store/data/mailbox/
71
+ example/store/data/sessions/
72
+ example/store/.tina4/
73
+ example/store/__pycache__/
74
+ example/store/src/**/__pycache__/
64
75
  .claude/settings.local.json
65
76
  .claude/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.97
3
+ Version: 3.11.0
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -749,6 +749,28 @@ Tina4 ships identical features across four languages — same architecture, same
749
749
 
750
750
  ---
751
751
 
752
+ ## Demo Store
753
+
754
+ A complete e-commerce app lives in `example/`. It demonstrates every framework feature through a real-world use case.
755
+
756
+ ```bash
757
+ cd example
758
+ bash setup.sh # macOS/Linux
759
+ # or: setup.bat # Windows
760
+ .venv/bin/python app.py
761
+ ```
762
+
763
+ Open http://localhost:7146
764
+
765
+ | Role | Email | Password |
766
+ |------|-------|----------|
767
+ | Admin | admin@tina4store.com | admin123 |
768
+ | Customer | alice@example.com | customer123 |
769
+
770
+ See [`example/README.md`](example/README.md) for full details.
771
+
772
+ ---
773
+
752
774
  ## Documentation
753
775
 
754
776
  Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
@@ -717,6 +717,28 @@ Tina4 ships identical features across four languages — same architecture, same
717
717
 
718
718
  ---
719
719
 
720
+ ## Demo Store
721
+
722
+ A complete e-commerce app lives in `example/`. It demonstrates every framework feature through a real-world use case.
723
+
724
+ ```bash
725
+ cd example
726
+ bash setup.sh # macOS/Linux
727
+ # or: setup.bat # Windows
728
+ .venv/bin/python app.py
729
+ ```
730
+
731
+ Open http://localhost:7146
732
+
733
+ | Role | Email | Password |
734
+ |------|-------|----------|
735
+ | Admin | admin@tina4store.com | admin123 |
736
+ | Customer | alice@example.com | customer123 |
737
+
738
+ See [`example/README.md`](example/README.md) for full details.
739
+
740
+ ---
741
+
720
742
  ## Documentation
721
743
 
722
744
  Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
@@ -245,6 +245,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
245
245
  | Dependency injection | `Container` from `tina4_python.container` |
246
246
  | Structured logging | `Log` from `tina4_python.debug` |
247
247
  | Error overlay (dev) | `render_error_overlay`, `is_debug_mode` from `tina4_python.debug.error_overlay` |
248
+ | Periodic background work | `background()` from `tina4_python.core.server` |
248
249
 
249
250
  **Bad — writing a custom queue:**
250
251
  ```python
@@ -289,6 +290,7 @@ Queue(topic="tasks").push({"action": "send_email"})
289
290
  6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `DATABASE_URL`.
290
291
  7. **Running the app**: `uv run python app.py <port> <name>` — port and name are CLI args handled by tina4_python
291
292
  8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
293
+ 12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
292
294
 
293
295
 
294
296
  ---
@@ -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.10.97"
11
+ __version__ = "3.11.0"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -60,4 +60,4 @@ from tina4_python.cache import ( # noqa: E402, F401
60
60
  from tina4_python.container import Container # noqa: E402, F401
61
61
 
62
62
  # ── Server ──
63
- from tina4_python.core.server import run # noqa: E402, F401
63
+ from tina4_python.core.server import run, background # noqa: E402, F401
@@ -32,6 +32,21 @@ _ai_port_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar("_ai_port_ct
32
32
  # Track startup time
33
33
  _start_time: float = 0
34
34
 
35
+ # ── Background tasks registry ────────────────────────────────────────────
36
+ _background_tasks: list[dict] = []
37
+
38
+
39
+ def background(callback, interval: float = 1.0):
40
+ """Register a background task that runs periodically in the server event loop.
41
+
42
+ Matches PHP's $app->background(fn, interval) pattern.
43
+
44
+ Args:
45
+ callback: Function to call (sync or async, no arguments).
46
+ interval: Seconds between invocations (default: 1.0).
47
+ """
48
+ _background_tasks.append({"callback": callback, "interval": interval})
49
+
35
50
 
36
51
  def _auto_discover(root_dir: str = "src"):
37
52
  """Auto-import all .py files in src/ to trigger route decorators."""
@@ -1812,7 +1827,48 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
1812
1827
  except NotImplementedError:
1813
1828
  pass # Windows
1814
1829
 
1830
+ # Start registered background tasks as asyncio tasks.
1831
+ # Sync callbacks run in a thread pool so they CANNOT block the event loop.
1832
+ # A timeout (2x interval, min 5s) cancels runaway tasks.
1833
+ import concurrent.futures
1834
+ _executor = concurrent.futures.ThreadPoolExecutor(
1835
+ max_workers=max(len(_background_tasks), 2),
1836
+ thread_name_prefix="tina4_bg",
1837
+ )
1838
+ bg_tasks = []
1839
+ for task_def in _background_tasks:
1840
+ cb = task_def["callback"]
1841
+ iv = task_def["interval"]
1842
+
1843
+ async def _tick_loop(_cb=cb, _iv=iv):
1844
+ timeout = max(_iv * 2, 5.0)
1845
+ while not shutdown.is_set():
1846
+ try:
1847
+ if asyncio.iscoroutinefunction(_cb):
1848
+ await asyncio.wait_for(_cb(), timeout=timeout)
1849
+ else:
1850
+ # Run sync callback in thread pool — never blocks the event loop
1851
+ await asyncio.wait_for(
1852
+ loop.run_in_executor(_executor, _cb),
1853
+ timeout=timeout,
1854
+ )
1855
+ except asyncio.TimeoutError:
1856
+ Log.warning(
1857
+ f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
1858
+ f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
1859
+ )
1860
+ except Exception as e:
1861
+ Log.error(f"Background task error: {e}")
1862
+ await asyncio.sleep(_iv)
1863
+
1864
+ bg_tasks.append(asyncio.create_task(_tick_loop()))
1865
+
1815
1866
  await shutdown.wait()
1867
+
1868
+ # Cancel background tasks
1869
+ for t in bg_tasks:
1870
+ t.cancel()
1871
+
1816
1872
  if ai_server:
1817
1873
  ai_server.close()
1818
1874
  await ai_server.wait_closed()
@@ -914,8 +914,40 @@ def _parse_filter_chain(expr: str) -> tuple[str, list[tuple[str, list[str]]]]:
914
914
  return variable, filters
915
915
 
916
916
 
917
- def _parse_args(raw: str) -> list[str]:
918
- """Parse filter arguments, respecting quoted strings and backslash escapes."""
917
+ def _coerce_arg(s: str):
918
+ """Try to convert a string argument to a Python object (dict, list, number, bool).
919
+
920
+ Returns the original string unchanged if no conversion applies.
921
+ """
922
+ if s.startswith("{") and s.endswith("}"):
923
+ try:
924
+ return json.loads(s)
925
+ except Exception:
926
+ pass
927
+ if s.startswith("[") and s.endswith("]"):
928
+ try:
929
+ return json.loads(s)
930
+ except Exception:
931
+ pass
932
+ if s in ("true", "True"):
933
+ return True
934
+ if s in ("false", "False"):
935
+ return False
936
+ if s in ("null", "None", "none"):
937
+ return None
938
+ try:
939
+ return int(s)
940
+ except (ValueError, TypeError):
941
+ pass
942
+ try:
943
+ return float(s)
944
+ except (ValueError, TypeError):
945
+ pass
946
+ return s
947
+
948
+
949
+ def _parse_args(raw: str) -> list:
950
+ """Parse filter arguments, respecting quoted strings, braces, and backslash escapes."""
919
951
  args = []
920
952
  current = ""
921
953
  in_quote = None
@@ -926,9 +958,9 @@ def _parse_args(raw: str) -> list[str]:
926
958
  in_quote = ch
927
959
  elif ch == in_quote:
928
960
  in_quote = None
929
- elif ch == "(" and not in_quote:
961
+ elif ch in ("(", "{", "[") and not in_quote:
930
962
  depth += 1
931
- elif ch == ")" and not in_quote:
963
+ elif ch in (")", "}", "]") and not in_quote:
932
964
  depth -= 1
933
965
  elif ch == "," and not in_quote and depth == 0:
934
966
  args.append(_strip_outer_quotes(current.strip()))
@@ -939,7 +971,7 @@ def _parse_args(raw: str) -> list[str]:
939
971
  if current.strip():
940
972
  args.append(_strip_outer_quotes(current.strip()))
941
973
 
942
- return args
974
+ return [_coerce_arg(a) for a in args]
943
975
 
944
976
 
945
977
  def _strip_outer_quotes(s: str) -> str:
@@ -964,6 +996,13 @@ def _strip_outer_quotes(s: str) -> str:
964
996
  return s
965
997
 
966
998
 
999
+ def _dict_replace(s: str, mapping: dict) -> str:
1000
+ """Apply multiple replacements from a dict (Twig-style replace filter)."""
1001
+ for old, new in mapping.items():
1002
+ s = s.replace(str(old), str(new))
1003
+ return s
1004
+
1005
+
967
1006
  # Built-in filters
968
1007
  _BUILTIN_FILTERS = {
969
1008
  "upper": lambda v, *a: str(v).upper(),
@@ -981,7 +1020,11 @@ _BUILTIN_FILTERS = {
981
1020
  "last": lambda v, *a: v[-1] if v else None,
982
1021
  "join": lambda v, *a: (a[0] if a else ", ").join(str(i) for i in v) if isinstance(v, list) else str(v),
983
1022
  "split": lambda v, *a: str(v).split(a[0] if a else " "),
984
- "replace": lambda v, *a: str(v).replace(a[0], a[1]) if len(a) >= 2 else str(v),
1023
+ "replace": lambda v, *a: (
1024
+ _dict_replace(str(v), a[0]) if len(a) == 1 and isinstance(a[0], dict)
1025
+ else str(v).replace(a[0], a[1]) if len(a) >= 2
1026
+ else str(v)
1027
+ ),
985
1028
  "default": lambda v, *a: v if v is not None and v != "" else (a[0] if a else ""),
986
1029
  "raw": lambda v, *a: v, # Mark as safe (no escaping)
987
1030
  "safe": lambda v, *a: v,
@@ -168,7 +168,7 @@ class ORM(metaclass=ORMMeta):
168
168
  table_name: str = ""
169
169
  soft_delete: bool = False # Set True to enable soft delete
170
170
  field_mapping: dict[str, str] = {} # {"python_attribute": "db_column"}
171
- auto_map: bool = False # No-op in Python (snake_case matches DB); exists for cross-language parity
171
+ auto_map: bool = True # No-op in Python (snake_case matches DB); exists for cross-language parity
172
172
  auto_crud: bool = False # Set True to auto-register CRUD routes
173
173
  _db: str | object | None = None # Per-model database override
174
174
  _fields: dict[str, Field] = {}
@@ -933,14 +933,18 @@ class ORM(metaclass=ORMMeta):
933
933
 
934
934
  # ── Serialization ───────────────────────────────────────────
935
935
 
936
- def to_dict(self, include: list[str] = None) -> dict:
936
+ def to_dict(self, include: list[str] = None, case: str = "snake") -> dict:
937
937
  """Convert to dict (field values only, optionally with relationships).
938
938
 
939
939
  Args:
940
940
  include: List of relationship names to include. Supports dot notation
941
941
  for nested relationships (e.g., ["posts.comments"]).
942
+ case: Key casing — 'snake' (default for Python), 'camel' (matches PHP).
942
943
  """
943
- result = {name: getattr(self, name) for name in self._fields}
944
+ if case == "camel":
945
+ result = {snake_to_camel(name): getattr(self, name) for name in self._fields}
946
+ else:
947
+ result = {name: getattr(self, name) for name in self._fields}
944
948
 
945
949
  if include:
946
950
  # Group includes: top-level and nested
@@ -957,27 +961,28 @@ class ORM(metaclass=ORMMeta):
957
961
  if rel_name in self._relationships:
958
962
  # Access the relationship (triggers lazy load if not cached)
959
963
  related = getattr(self, rel_name)
964
+ key = snake_to_camel(rel_name) if case == "camel" else rel_name
960
965
  if related is None:
961
- result[rel_name] = None
966
+ result[key] = None
962
967
  elif isinstance(related, list):
963
- result[rel_name] = [
964
- r.to_dict(include=nested if nested else None)
968
+ result[key] = [
969
+ r.to_dict(include=nested if nested else None, case=case)
965
970
  for r in related
966
971
  ]
967
972
  else:
968
- result[rel_name] = related.to_dict(
969
- include=nested if nested else None
973
+ result[key] = related.to_dict(
974
+ include=nested if nested else None, case=case
970
975
  )
971
976
 
972
977
  return result
973
978
 
974
- def to_assoc(self, include: list[str] = None) -> dict:
979
+ def to_assoc(self, include: list[str] = None, case: str = "snake") -> dict:
975
980
  """Convert to an associative dict (alias for to_dict)."""
976
- return self.to_dict(include=include)
981
+ return self.to_dict(include=include, case=case)
977
982
 
978
- def to_object(self) -> dict:
983
+ def to_object(self, case: str = "snake") -> dict:
979
984
  """Convert to an object/dict (alias for to_dict)."""
980
- return self.to_dict()
985
+ return self.to_dict(case=case)
981
986
 
982
987
  def to_array(self) -> list:
983
988
  """Convert to a list of values."""
@@ -62,10 +62,15 @@ def compile_scss(scss_dir: str = "src/scss", output: str = "public/css/default.c
62
62
  if minify:
63
63
  css = _minify(css)
64
64
 
65
- # Write output
65
+ # Write output only if content changed (avoids triggering DevReload loops)
66
66
  out_path = Path(output)
67
67
  out_path.parent.mkdir(parents=True, exist_ok=True)
68
- out_path.write_text(css, encoding="utf-8")
68
+ try:
69
+ existing = out_path.read_text(encoding="utf-8")
70
+ except (FileNotFoundError, OSError):
71
+ existing = None
72
+ if existing != css:
73
+ out_path.write_text(css, encoding="utf-8")
69
74
 
70
75
  return css
71
76