tina4-python 3.9.2__tar.gz → 3.10.1__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 (139) hide show
  1. {tina4_python-3.9.2 → tina4_python-3.10.1}/PKG-INFO +1 -1
  2. {tina4_python-3.9.2 → tina4_python-3.10.1}/pyproject.toml +1 -1
  3. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/CLAUDE.md +11 -2
  4. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/response.py +75 -21
  5. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/router.py +41 -18
  6. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/server.py +12 -17
  7. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/messenger/__init__.py +2 -3
  8. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/orm/model.py +17 -0
  9. {tina4_python-3.9.2 → tina4_python-3.10.1}/.gitignore +0 -0
  10. {tina4_python-3.9.2 → tina4_python-3.10.1}/README.md +0 -0
  11. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/__init__.py +0 -0
  14. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/dev_reload.py +0 -0
  40. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/frond/engine.py +0 -0
  44. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/migration/__init__.py +0 -0
  63. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/migration/runner.py +0 -0
  64. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/orm/__init__.py +0 -0
  65. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/orm/fields.py +0 -0
  66. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/css/tina4.css +0 -0
  67. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/css/tina4.min.css +0 -0
  68. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/favicon.ico +0 -0
  69. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/images/logo.svg +0 -0
  70. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  71. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/js/frond.min.js +0 -0
  72. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  73. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/js/tina4.min.js +0 -0
  74. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/js/tina4js.min.js +0 -0
  75. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/swagger/index.html +0 -0
  76. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  77. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/query_builder/__init__.py +0 -0
  78. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/queue/__init__.py +0 -0
  79. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/queue_backends/__init__.py +0 -0
  80. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/queue_backends/kafka_backend.py +0 -0
  81. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/queue_backends/mongo_backend.py +0 -0
  82. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  83. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/__init__.py +0 -0
  84. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  85. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_badges.scss +0 -0
  86. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  87. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_cards.scss +0 -0
  88. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_forms.scss +0 -0
  89. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_grid.scss +0 -0
  90. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_modals.scss +0 -0
  91. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_nav.scss +0 -0
  92. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_reset.scss +0 -0
  93. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_tables.scss +0 -0
  94. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_typography.scss +0 -0
  95. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  96. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/_variables.scss +0 -0
  97. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/base.scss +0 -0
  98. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/colors.scss +0 -0
  99. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/scss/tina4css/tina4.scss +0 -0
  100. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/seeder/__init__.py +0 -0
  101. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/service/__init__.py +0 -0
  102. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/session/__init__.py +0 -0
  103. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/session_handlers/__init__.py +0 -0
  104. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  105. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/session_handlers/redis_handler.py +0 -0
  106. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/session_handlers/valkey_handler.py +0 -0
  107. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/swagger/__init__.py +0 -0
  108. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/components/crud.twig +0 -0
  109. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  110. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  111. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/docker/python/Dockerfile +0 -0
  112. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  113. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/302.twig +0 -0
  114. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/401.twig +0 -0
  115. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/403.twig +0 -0
  116. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/404.twig +0 -0
  117. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/500.twig +0 -0
  118. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/502.twig +0 -0
  119. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/503.twig +0 -0
  120. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/errors/base.twig +0 -0
  121. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/frontend/README.md +0 -0
  122. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/templates/readme.md +0 -0
  123. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/test_client/__init__.py +0 -0
  124. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  125. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  126. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  127. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  128. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/validator/__init__.py +0 -0
  137. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/websocket/__init__.py +0 -0
  138. {tina4_python-3.9.2 → tina4_python-3.10.1}/tina4_python/websocket/backplane.py +0 -0
  139. {tina4_python-3.9.2 → tina4_python-3.10.1}/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.9.2
3
+ Version: 3.10.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.9.2"
3
+ version = "3.10.1"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -233,7 +233,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
233
233
  | GraphQL API | `GraphQL` from `tina4_python.graphql` |
234
234
  | SOAP/WSDL services | `WSDL` from `tina4_python.wsdl` |
235
235
  | Database migrations | `migrate`, `create_migration` from `tina4_python.migration` |
236
- | WebSockets | `WebSocketServer` from `tina4_python.websocket` |
236
+ | WebSockets | `WebSocketServer` from `tina4_python.websocket`. Backplane via Redis pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`) |
237
237
  | SCSS compilation | Drop `.scss` in `src/scss/` — auto-compiled |
238
238
  | Static file serving | Put files in `src/public/` — auto-served |
239
239
  | Translations / i18n | `I18n` from `tina4_python.i18n` |
@@ -336,7 +336,7 @@ tina4_python/ # Core framework package (v3.0.0)
336
336
  ├── migration/ # SQL-file migrations (migrate, create_migration, rollback)
337
337
  │ └── runner.py # Migration runner
338
338
  ├── session/ # Pluggable sessions (Session, FileSessionHandler, DatabaseSessionHandler)
339
- ├── websocket/ # RFC 6455 WebSocket server (WebSocketServer, WebSocketConnection)
339
+ ├── websocket/ # RFC 6455 WebSocket server (WebSocketServer, WebSocketConnection, backplane)
340
340
  ├── graphql/ # Zero-dep GraphQL engine (GraphQL, Schema)
341
341
  ├── wsdl/ # SOAP 1.1 / WSDL server (WSDL, wsdl_operation)
342
342
  ├── crud/ # Auto-CRUD REST endpoint generator (AutoCrud)
@@ -504,6 +504,7 @@ TINA4_SESSION_MONGO_USERNAME= # optional
504
504
  TINA4_SESSION_MONGO_PASSWORD= # optional
505
505
  TINA4_SESSION_MONGO_DB=tina4_sessions # default database
506
506
  TINA4_SESSION_MONGO_COLLECTION=sessions # default collection
507
+ TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
507
508
  ```
508
509
 
509
510
  ### Authentication & Security
@@ -836,6 +837,9 @@ result = User().select(filter="name = ?", params=["Alice"], limit=10)
836
837
 
837
838
  # Convert to dict
838
839
  user.to_dict()
840
+
841
+ # NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
842
+ result.to_mongo()
839
843
  ```
840
844
 
841
845
  ### Available field types
@@ -1532,6 +1536,7 @@ HOST_NAME=localhost:7145
1532
1536
 
1533
1537
  # Sessions
1534
1538
  TINA4_SESSION_HANDLER=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
1539
+ TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
1535
1540
 
1536
1541
  # Swagger/OpenAPI
1537
1542
  SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
@@ -1768,6 +1773,10 @@ async def dashboard(request, response):
1768
1773
  - **Messenger**: .env driven SMTP/IMAP
1769
1774
  - **ORM relationships**: `has_many`, `has_one`, `belongs_to` with eager loading (`include=`)
1770
1775
  - **Frond pre-compilation**: 2.8x template render improvement, `Frond.clear_cache()`
1776
+ - **QueryBuilder** with NoSQL/MongoDB support (`to_mongo()`)
1777
+ - **WebSocket backplane** (Redis pub/sub) for horizontal scaling
1778
+ - **SameSite=Lax** default on session cookies (`TINA4_SESSION_SAMESITE`)
1779
+ - **`tina4 init`** generates Dockerfile and .dockerignore
1771
1780
  - **Gallery**: 7 interactive examples with Try It deploy at `/__dev/`
1772
1781
 
1773
1782
 
@@ -22,6 +22,54 @@ import mimetypes
22
22
  from pathlib import Path
23
23
 
24
24
 
25
+ # ---------------------------------------------------------------------------
26
+ # Global Frond template engine registry
27
+ # ---------------------------------------------------------------------------
28
+ _global_frond = None
29
+ _framework_frond = None
30
+
31
+
32
+ def get_frond():
33
+ """Return the global Frond engine, creating a default if needed."""
34
+ global _global_frond
35
+ if _global_frond is None:
36
+ from tina4_python.frond.engine import Frond
37
+ _global_frond = Frond("src/templates")
38
+ return _global_frond
39
+
40
+
41
+ def get_framework_frond():
42
+ """Return the singleton Frond engine for built-in framework templates."""
43
+ global _framework_frond
44
+ framework_dir = Path(__file__).resolve().parent.parent / "templates"
45
+ if _framework_frond is None and framework_dir.is_dir():
46
+ from tina4_python.frond.engine import Frond
47
+ _framework_frond = Frond(str(framework_dir))
48
+ # Sync custom filters/globals from the user engine
49
+ if _framework_frond is not None:
50
+ user_engine = get_frond()
51
+ _framework_frond._filters.update(user_engine._filters)
52
+ _framework_frond._globals.update(user_engine._globals)
53
+ return _framework_frond
54
+
55
+
56
+ def set_frond(engine):
57
+ """Register a pre-configured Frond engine for response.render().
58
+
59
+ Call this at startup after registering custom filters and globals:
60
+
61
+ from tina4_python.frond import Frond
62
+ from tina4_python.core.response import set_frond
63
+
64
+ engine = Frond("src/templates")
65
+ engine.add_filter("money", my_money_filter)
66
+ engine.add_global("APP_VERSION", "1.0")
67
+ set_frond(engine)
68
+ """
69
+ global _global_frond
70
+ _global_frond = engine
71
+
72
+
25
73
  class Response:
26
74
  """HTTP response builder with compression and ETag support."""
27
75
 
@@ -175,27 +223,33 @@ class Response:
175
223
  return self
176
224
 
177
225
  def render(self, template: str, data: dict = None) -> "Response":
178
- """Render a Frond/Twig template with data."""
179
- from tina4_python.frond.engine import Frond
180
- from pathlib import Path
181
-
182
- # Search for templates in user dir first, then framework dir
183
- template_dirs = []
184
- user_dir = Path("src/templates")
185
- if user_dir.is_dir():
186
- template_dirs.append(str(user_dir))
187
- framework_dir = Path(__file__).resolve().parent.parent / "templates"
188
- if framework_dir.is_dir():
189
- template_dirs.append(str(framework_dir))
190
-
191
- for tdir in template_dirs:
192
- if (Path(tdir) / template).exists():
193
- try:
194
- frond = Frond(tdir)
195
- html = frond.render(template, data or {})
196
- return self.html(html)
197
- except Exception as e:
198
- return self.html(f"<pre>Template error: {e}</pre>", 500)
226
+ """Render a Frond/Twig template with data.
227
+
228
+ Uses the global Frond engine (registered via set_frond()) so that
229
+ custom filters and globals are available in all templates.
230
+ Falls back to framework templates if not found in user dir.
231
+ """
232
+ engine = get_frond()
233
+
234
+ # Try user templates first (the global engine's directory)
235
+ try:
236
+ html = engine.render(template, data or {})
237
+ return self.html(html)
238
+ except FileNotFoundError:
239
+ pass
240
+ except Exception as e:
241
+ return self.html(f"<pre>Template error: {e}</pre>", 500)
242
+
243
+ # Fallback: framework templates (singleton, filters/globals synced)
244
+ fw_engine = get_framework_frond()
245
+ if fw_engine is not None:
246
+ try:
247
+ html = fw_engine.render(template, data or {})
248
+ return self.html(html)
249
+ except FileNotFoundError:
250
+ pass
251
+ except Exception as e:
252
+ return self.html(f"<pre>Template error: {e}</pre>", 500)
199
253
 
200
254
  return self.html(f"<pre>Template not found: {template}</pre>", 404)
201
255
 
@@ -180,10 +180,12 @@ class Router:
180
180
  if cls._group_prefix:
181
181
  path = cls._group_prefix + path
182
182
 
183
- # Merge group middleware with route-level middleware
184
- if cls._group_middleware:
185
- route_mw = options.get("middleware", [])
186
- options["middleware"] = list(cls._group_middleware) + list(route_mw)
183
+ # Merge group middleware with route-level middleware and handler-level middleware
184
+ handler_mw = getattr(handler, "_middleware", [])
185
+ route_mw = options.get("middleware", [])
186
+ combined_mw = list(cls._group_middleware) + list(handler_mw) + list(route_mw)
187
+ if combined_mw:
188
+ options["middleware"] = combined_mw
187
189
 
188
190
  pattern, param_names = _compile_pattern(path)
189
191
 
@@ -251,8 +253,14 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
251
253
  param_names = []
252
254
  regex_parts = []
253
255
 
254
- for segment in path.strip("/").split("/"):
255
- if segment.startswith("{") and segment.endswith("}"):
256
+ segments = path.strip("/").split("/")
257
+ for i, segment in enumerate(segments):
258
+ if segment == "*":
259
+ # Wildcard: matches the rest of the path (greedy)
260
+ param_names.append("wildcard")
261
+ regex_parts.append("(.+)")
262
+ break # Nothing can follow a wildcard
263
+ elif segment.startswith("{") and segment.endswith("}"):
256
264
  inner = segment[1:-1]
257
265
  if ":" in inner:
258
266
  name, type_hint = inner.split(":", 1)
@@ -277,51 +285,53 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
277
285
 
278
286
  # Decorator functions — the public API
279
287
 
288
+ def _register_route(method: str, path: str, fn, **options):
289
+ """Common registration logic that preserves handler attributes on the returned ref."""
290
+ ref = Router.add(method, path, fn, **options)
291
+ # Propagate handler attributes to the wrapper so stacked decorators still work
292
+ fn._route_ref = ref
293
+ return fn
294
+
295
+
280
296
  def get(path: str, **options):
281
297
  """Register a GET route."""
282
298
  def decorator(fn):
283
- Router.add("GET", path, fn, **options)
284
- return fn
299
+ return _register_route("GET", path, fn, **options)
285
300
  return decorator
286
301
 
287
302
 
288
303
  def post(path: str, **options):
289
304
  """Register a POST route."""
290
305
  def decorator(fn):
291
- Router.add("POST", path, fn, **options)
292
- return fn
306
+ return _register_route("POST", path, fn, **options)
293
307
  return decorator
294
308
 
295
309
 
296
310
  def put(path: str, **options):
297
311
  """Register a PUT route."""
298
312
  def decorator(fn):
299
- Router.add("PUT", path, fn, **options)
300
- return fn
313
+ return _register_route("PUT", path, fn, **options)
301
314
  return decorator
302
315
 
303
316
 
304
317
  def patch(path: str, **options):
305
318
  """Register a PATCH route."""
306
319
  def decorator(fn):
307
- Router.add("PATCH", path, fn, **options)
308
- return fn
320
+ return _register_route("PATCH", path, fn, **options)
309
321
  return decorator
310
322
 
311
323
 
312
324
  def delete(path: str, **options):
313
325
  """Register a DELETE route."""
314
326
  def decorator(fn):
315
- Router.add("DELETE", path, fn, **options)
316
- return fn
327
+ return _register_route("DELETE", path, fn, **options)
317
328
  return decorator
318
329
 
319
330
 
320
331
  def any_method(path: str, **options):
321
332
  """Register a route for any HTTP method."""
322
333
  def decorator(fn):
323
- Router.add("ANY", path, fn, **options)
324
- return fn
334
+ return _register_route("ANY", path, fn, **options)
325
335
  return decorator
326
336
 
327
337
  # Alias — @any() is the standard name across all Tina4 frameworks
@@ -352,6 +362,10 @@ def noauth():
352
362
  """Make a write route (POST/PUT/PATCH/DELETE) public — no auth required."""
353
363
  def decorator(fn):
354
364
  fn._noauth = True
365
+ # If route was already registered (decorator applied after @get/@post),
366
+ # update the route dict directly.
367
+ if hasattr(fn, "_route_ref"):
368
+ fn._route_ref._route["auth_required"] = False
355
369
  return fn
356
370
  return decorator
357
371
 
@@ -360,6 +374,10 @@ def secured():
360
374
  """Require auth on a GET route (which is public by default)."""
361
375
  def decorator(fn):
362
376
  fn._secured = True
377
+ # If route was already registered (decorator applied after @get/@post),
378
+ # update the route dict directly.
379
+ if hasattr(fn, "_route_ref"):
380
+ fn._route_ref._route["auth_required"] = True
363
381
  return fn
364
382
  return decorator
365
383
 
@@ -370,6 +388,11 @@ def middleware(*middleware_classes):
370
388
  """Attach middleware classes to a route handler."""
371
389
  def decorator(fn):
372
390
  fn._middleware = list(middleware_classes)
391
+ # If route was already registered (decorator applied after @get/@post),
392
+ # update the route dict directly.
393
+ if hasattr(fn, "_route_ref"):
394
+ existing = fn._route_ref._route.get("middleware", [])
395
+ fn._route_ref._route["middleware"] = list(middleware_classes) + existing
373
396
  return fn
374
397
  return decorator
375
398
 
@@ -105,7 +105,7 @@ def _render_error_page(status_code: int, path: str, request_id: str, error_messa
105
105
 
106
106
  Returns rendered HTML string, or None if no template found.
107
107
  """
108
- from tina4_python.frond.engine import Frond
108
+ from tina4_python.core.response import get_frond, get_framework_frond
109
109
 
110
110
  template_name = f"errors/{status_code}.twig"
111
111
  data = {
@@ -115,21 +115,17 @@ def _render_error_page(status_code: int, path: str, request_id: str, error_messa
115
115
  "status_code": status_code,
116
116
  }
117
117
 
118
- # 1. Try user override
119
- user_dir = Path("src/templates")
120
- if (user_dir / template_name).exists():
121
- try:
122
- engine = Frond(str(user_dir))
123
- return engine.render(template_name, data)
124
- except Exception:
125
- pass
118
+ # 1. Try user override (singleton engine with custom filters/globals)
119
+ try:
120
+ return get_frond().render(template_name, data)
121
+ except (FileNotFoundError, Exception):
122
+ pass
126
123
 
127
- # 2. Try framework default
128
- framework_dir = Path(__file__).resolve().parent.parent / "templates"
129
- if (framework_dir / template_name).exists():
124
+ # 2. Try framework default (singleton, filters/globals synced)
125
+ fw_engine = get_framework_frond()
126
+ if fw_engine is not None:
130
127
  try:
131
- engine = Frond(str(framework_dir))
132
- return engine.render(template_name, data)
128
+ return fw_engine.render(template_name, data)
133
129
  except Exception:
134
130
  pass
135
131
 
@@ -848,9 +844,8 @@ async def app(scope: dict, receive, send):
848
844
  # Try serving a template file (e.g. /hello -> src/templates/hello.twig or hello.html)
849
845
  tpl_file = _resolve_template(request.path)
850
846
  if tpl_file:
851
- from tina4_python.frond import Frond
852
- frond = Frond()
853
- html = frond.render(tpl_file, {})
847
+ from tina4_python.core.response import get_frond
848
+ html = get_frond().render(tpl_file, {})
854
849
  response.html(html)
855
850
  elif request.path == "/":
856
851
  response.html(_render_landing_page())
@@ -203,9 +203,8 @@ class Messenger:
203
203
  **kwargs: Passed to send() (cc, bcc, attachments, etc.)
204
204
  """
205
205
  try:
206
- from tina4_python.frond import Frond
207
- engine = Frond()
208
- body = engine.render_string(template, data or {})
206
+ from tina4_python.core.response import get_frond
207
+ body = get_frond().render_string(template, data or {})
209
208
  except ImportError:
210
209
  body = template
211
210
 
@@ -43,6 +43,22 @@ def orm_bind(db, name: str = None):
43
43
  _databases[name] = db
44
44
 
45
45
 
46
+ def snake_to_camel(name: str) -> str:
47
+ """Convert snake_case to camelCase: 'first_name' -> 'firstName'."""
48
+ parts = name.split("_")
49
+ return parts[0] + "".join(p.capitalize() for p in parts[1:])
50
+
51
+
52
+ def camel_to_snake(name: str) -> str:
53
+ """Convert camelCase to snake_case: 'firstName' -> 'first_name'."""
54
+ result = []
55
+ for c in name:
56
+ if c.isupper() and result:
57
+ result.append("_")
58
+ result.append(c.lower())
59
+ return "".join(result)
60
+
61
+
46
62
  class ORMMeta(type):
47
63
  """Metaclass that collects Field definitions and relationship descriptors."""
48
64
 
@@ -79,6 +95,7 @@ class ORM(metaclass=ORMMeta):
79
95
  table_name: str = ""
80
96
  soft_delete: bool = False # Set True to enable soft delete
81
97
  field_mapping: dict[str, str] = {} # {"python_attribute": "db_column"}
98
+ auto_map: bool = False # No-op in Python (snake_case matches DB); exists for cross-language parity
82
99
  _db: str | object | None = None # Per-model database override
83
100
  _fields: dict[str, Field] = {}
84
101
 
File without changes
File without changes