tina4-python 3.1.3__tar.gz → 3.3.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 (134) hide show
  1. {tina4_python-3.1.3 → tina4_python-3.3.0}/PKG-INFO +2 -2
  2. {tina4_python-3.1.3 → tina4_python-3.3.0}/README.md +1 -1
  3. {tina4_python-3.1.3 → tina4_python-3.3.0}/pyproject.toml +1 -1
  4. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/CLAUDE.md +27 -33
  5. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/ai/__init__.py +1 -1
  7. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/auth/__init__.py +14 -5
  8. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/cli/__init__.py +37 -1
  9. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/__init__.py +2 -2
  10. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/request.py +14 -6
  11. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/router.py +175 -3
  12. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/server.py +298 -5
  13. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/adapter.py +186 -0
  14. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/connection.py +2 -1
  15. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/firebird.py +3 -1
  16. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/mssql.py +3 -1
  17. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/mysql.py +3 -1
  18. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/odbc.py +3 -1
  19. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/postgres.py +3 -1
  20. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/sqlite.py +3 -1
  21. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/debug/error_overlay.py +10 -6
  22. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/dev_admin/__init__.py +44 -14
  23. tina4_python-3.3.0/tina4_python/dev_reload.py +206 -0
  24. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/frond/engine.py +128 -1
  25. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +3 -6
  26. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/migration/__init__.py +2 -2
  27. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/migration/runner.py +66 -20
  28. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/orm/model.py +49 -15
  29. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/js/tina4-dev-admin.min.js +1 -1
  30. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/queue/__init__.py +151 -94
  31. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/queue_backends/__init__.py +4 -3
  32. tina4_python-3.3.0/tina4_python/queue_backends/mongo_backend.py +210 -0
  33. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/session/__init__.py +21 -1
  34. {tina4_python-3.1.3 → tina4_python-3.3.0}/.gitignore +0 -0
  35. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/HtmlElement.py +0 -0
  36. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/Testing.py +0 -0
  37. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/api/__init__.py +0 -0
  38. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/cache/__init__.py +0 -0
  39. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/container/__init__.py +0 -0
  40. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/cache.py +0 -0
  41. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/constants.py +0 -0
  42. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/events.py +0 -0
  43. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/middleware.py +0 -0
  44. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/core/response.py +0 -0
  45. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/crud/__init__.py +0 -0
  46. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/database/__init__.py +0 -0
  47. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/debug/__init__.py +0 -0
  48. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/dotenv/__init__.py +0 -0
  49. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/auth/meta.json +0 -0
  52. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  53. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/database/meta.json +0 -0
  54. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  55. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
  56. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  57. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/orm/meta.json +0 -0
  58. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  59. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  60. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/queue/meta.json +0 -0
  61. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/messenger/__init__.py +0 -0
  69. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/orm/__init__.py +0 -0
  70. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/orm/fields.py +0 -0
  71. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/css/tina4.css +0 -0
  72. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/css/tina4.min.css +0 -0
  73. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/favicon.ico +0 -0
  74. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/images/logo.svg +0 -0
  75. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  76. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/js/frond.min.js +0 -0
  77. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/js/tina4.min.js +0 -0
  78. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/swagger/index.html +0 -0
  79. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  80. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
  81. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  82. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/__init__.py +0 -0
  83. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  84. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
  85. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  86. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
  87. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
  88. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
  89. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
  90. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
  91. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
  92. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
  93. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
  94. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  95. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
  96. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/base.scss +0 -0
  97. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/colors.scss +0 -0
  98. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
  99. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/seeder/__init__.py +0 -0
  100. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/service/__init__.py +0 -0
  101. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/session_handlers/__init__.py +0 -0
  102. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  103. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/session_handlers/redis_handler.py +0 -0
  104. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
  105. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/swagger/__init__.py +0 -0
  106. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/components/crud.twig +0 -0
  107. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  108. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  109. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
  110. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  111. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/302.twig +0 -0
  112. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/401.twig +0 -0
  113. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/403.twig +0 -0
  114. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/404.twig +0 -0
  115. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/500.twig +0 -0
  116. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/502.twig +0 -0
  117. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/503.twig +0 -0
  118. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/errors/base.twig +0 -0
  119. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/frontend/README.md +0 -0
  120. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/templates/readme.md +0 -0
  121. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  122. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  123. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  124. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  125. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  126. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  127. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  128. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  129. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  130. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  131. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  132. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  133. {tina4_python-3.1.3 → tina4_python-3.3.0}/tina4_python/websocket/__init__.py +0 -0
  134. {tina4_python-3.1.3 → tina4_python-3.3.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.1.3
3
+ Version: 3.3.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
@@ -115,7 +115,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
115
115
  | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
116
116
  | **Auth** | Zero-dep JWT (HS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
117
117
  | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
118
- | **Background** | Queue (SQLite/RabbitMQ/Kafka) with priority, delayed jobs, retry, batch processing |
118
+ | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
119
119
  | **Real-time** | Native asyncio WebSocket (RFC 6455), per-path routing, connection manager |
120
120
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
121
121
  | **DX** | Dev admin dashboard (11 tabs), error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
@@ -83,7 +83,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
83
83
  | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
84
84
  | **Auth** | Zero-dep JWT (HS256), sessions (file/Redis/Valkey/MongoDB/database), password hashing, form tokens |
85
85
  | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE, WSDL/SOAP with auto WSDL |
86
- | **Background** | Queue (SQLite/RabbitMQ/Kafka) with priority, delayed jobs, retry, batch processing |
86
+ | **Background** | Queue (SQLite/RabbitMQ/Kafka/MongoDB) with priority, delayed jobs, retry, batch processing |
87
87
  | **Real-time** | Native asyncio WebSocket (RFC 6455), per-path routing, connection manager |
88
88
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
89
89
  | **DX** | Dev admin dashboard (11 tabs), error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.1.3"
3
+ version = "3.3.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"}
@@ -154,7 +154,7 @@ Never use raw `requests` or `urllib` directly. Use the built-in `Api` class —
154
154
 
155
155
  ### 7. Use Queues for Long-Running Work
156
156
 
157
- Route handlers must respond fast. Any operation that takes more than a second (sending emails, generating reports, calling slow external APIs, processing files) must be pushed to a Queue and processed by a Consumer.
157
+ Route handlers must respond fast. Any operation that takes more than a second (sending emails, generating reports, calling slow external APIs, processing files) must be pushed to a Queue and consumed via `queue.consume()`.
158
158
 
159
159
  **Bad — blocking the request:**
160
160
  ```python
@@ -170,8 +170,8 @@ async def generate_report(request, response):
170
170
  ```python
171
171
  @post("/api/reports")
172
172
  async def generate_report(request, response):
173
- producer = Producer(Queue(db, topic="reports"))
174
- producer.push({"user_id": request.body["user_id"], "type": "monthly"})
173
+ queue = Queue(topic="reports")
174
+ queue.push({"user_id": request.body["user_id"], "type": "monthly"})
175
175
  return response({"status": "queued"})
176
176
  ```
177
177
 
@@ -219,7 +219,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
219
219
 
220
220
  | Need | Use this — don't build your own |
221
221
  |------|--------------------------------|
222
- | Background jobs / async work | `Queue`, `Producer`, `Consumer` from `tina4_python.queue` |
222
+ | Background jobs / async work | `Queue` from `tina4_python.queue` (use `queue.push()`, `queue.consume()`) |
223
223
  | HTTP calls to external APIs | `Api` from `tina4_python.api` |
224
224
  | JWT tokens & auth | `Auth` from `tina4_python.auth` (create_token, validate_token, get_payload) |
225
225
  | Password hashing | `Auth.hash_password()` / `Auth.check_password()` from `tina4_python.auth` |
@@ -254,8 +254,8 @@ task_queue = queue.Queue() # Don't do this!
254
254
 
255
255
  **Good — use Tina4's Queue:**
256
256
  ```python
257
- from tina4_python.queue import Queue, Producer
258
- Producer(Queue(db, topic="tasks")).push({"action": "send_email"})
257
+ from tina4_python.queue import Queue
258
+ Queue(topic="tasks").push({"action": "send_email"})
259
259
  ```
260
260
 
261
261
  ### 11. Key tina4_python Gotchas
@@ -331,7 +331,7 @@ tina4_python/ # Core framework package (v3.0.0)
331
331
  ├── frond/ # Template engine (Frond — Jinja2/Twig-compatible)
332
332
  │ └── engine.py # Frond class (render, add_filter, add_global, add_test)
333
333
  ├── api/ # HTTP client (Api — urllib, zero deps)
334
- ├── queue/ # Database-backed job queue (Queue, Producer, Consumer)
334
+ ├── queue/ # Database-backed job queue (Queue, Job)
335
335
  ├── swagger/ # OpenAPI 3.0.3 generator (Swagger, description, tags, example)
336
336
  ├── migration/ # SQL-file migrations (migrate, create_migration, rollback)
337
337
  │ └── runner.py # Migration runner
@@ -1017,48 +1017,43 @@ async def admin_dashboard(request, response):
1017
1017
 
1018
1018
  Supports: litequeue (default/SQLite, zero-config), RabbitMQ, Kafka, MongoDB.
1019
1019
 
1020
- ### Producer — enqueue work from a route
1020
+ ### Producing — enqueue work from a route
1021
1021
 
1022
1022
  ```python
1023
- from tina4_python.queue import Queue, Producer
1023
+ from tina4_python.queue import Queue
1024
1024
 
1025
1025
  @post("/api/reports/generate")
1026
1026
  async def request_report(request, response):
1027
- queue = Queue(db, topic="reports")
1028
- producer = Producer(queue)
1029
- producer.push({
1027
+ queue = Queue(topic="reports")
1028
+ queue.push({
1030
1029
  "user_id": request.body["user_id"],
1031
1030
  "report_type": "monthly",
1032
1031
  })
1033
1032
  return response({"status": "queued"})
1034
1033
  ```
1035
1034
 
1036
- ### Consumer — process work in a background worker
1035
+ ### Consuming — process work in a background worker
1037
1036
 
1038
1037
  ```python
1039
1038
  # worker.py (run separately: python worker.py)
1040
- from tina4_python.queue import Queue, Consumer
1039
+ from tina4_python.queue import Queue
1041
1040
  from tina4_python.database import Database
1042
1041
 
1043
1042
  db = Database("sqlite:///app.db")
1043
+ queue = Queue(topic="reports")
1044
1044
 
1045
- def handle_report(job):
1045
+ for job in queue.consume("reports"):
1046
1046
  data = job.data
1047
1047
  report = generate_report(data["user_id"], data["report_type"])
1048
1048
  send_email(data["user_id"], report)
1049
-
1050
- queue = Queue(db, topic="reports")
1051
- consumer = Consumer(queue, callback=handle_report)
1052
- consumer.run_forever()
1049
+ job.complete()
1053
1050
  ```
1054
1051
 
1055
1052
  ### Poll once for available jobs
1056
1053
 
1057
1054
  ```python
1058
- queue = Queue(db, topic="logs")
1059
- consumer = Consumer(queue)
1060
- jobs = consumer.poll() # Returns list of Job objects
1061
- for job in jobs:
1055
+ queue = Queue(topic="logs")
1056
+ for job in queue.consume():
1062
1057
  process(job.data)
1063
1058
  job.complete()
1064
1059
  ```
@@ -1066,7 +1061,7 @@ for job in jobs:
1066
1061
  ### Queue management
1067
1062
 
1068
1063
  ```python
1069
- queue = Queue(db, topic="tasks", max_retries=3)
1064
+ queue = Queue(topic="tasks", max_retries=3)
1070
1065
 
1071
1066
  # Check queue size
1072
1067
  queue.size() # pending jobs
@@ -1681,7 +1676,7 @@ async def upload(request, response):
1681
1676
  file_list = uploaded if isinstance(uploaded, list) else [uploaded]
1682
1677
  for f in file_list:
1683
1678
  content = base64.b64decode(f["content"])
1684
- with open(os.path.join("src/public/uploads", f["file_name"]), "wb") as fh:
1679
+ with open(os.path.join("src/public/uploads", f["filename"]), "wb") as fh:
1685
1680
  fh.write(content)
1686
1681
  return response({"uploaded": len(file_list)})
1687
1682
  ```
@@ -1715,8 +1710,8 @@ async def charge(request, response):
1715
1710
  # In route — fast response
1716
1711
  @post("/api/invite")
1717
1712
  async def invite(request, response):
1718
- producer = Producer(Queue(db, topic="emails"))
1719
- producer.push({
1713
+ queue = Queue(topic="emails")
1714
+ queue.push({
1720
1715
  "to": request.body["email"],
1721
1716
  "template": "invite",
1722
1717
  "data": {"name": request.body["name"]}
@@ -1724,13 +1719,12 @@ async def invite(request, response):
1724
1719
  return response({"sent": True})
1725
1720
 
1726
1721
  # In worker — separate process
1727
- def send_email(message):
1728
- email = message.data
1722
+ queue = Queue(topic="emails")
1723
+ for job in queue.consume("emails"):
1724
+ email = job.data
1729
1725
  html = Template.render(f"emails/{email['template']}.twig", email["data"])
1730
1726
  # ... send via SMTP
1731
-
1732
- queue = Queue(topic="emails", callback=send_email)
1733
- Consumer(queue).run_forever()
1727
+ job.complete()
1734
1728
  ```
1735
1729
 
1736
1730
  ### Full page with template inheritance
@@ -1770,7 +1764,7 @@ async def dashboard(request, response):
1770
1764
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1771
1765
  - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
1772
1766
  - **Sessions**: 4 backends (file, Redis/Valkey, MongoDB, database)
1773
- - **Queue**: SQLite/RabbitMQ/Kafka backends, configured via env vars
1767
+ - **Queue**: SQLite/RabbitMQ/Kafka/MongoDB backends, configured via env vars
1774
1768
  - **Cache**: memory/Redis/file backends
1775
1769
  - **Messenger**: .env driven SMTP/IMAP
1776
1770
  - **ORM relationships**: `has_many`, `has_one`, `belongs_to` with eager loading (`include=`)
@@ -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.0.0-dev"
11
+ __version__ = "3.3.0"
12
12
 
13
13
  # ── HTTP Constants ──
14
14
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -169,7 +169,7 @@ frontend/ — Frontend framework source (builds to public/)
169
169
  | WebSocket | websocket | `from tina4_python.websocket import WebSocketServer` |
170
170
  | SOAP/WSDL | wsdl | `from tina4_python.wsdl import WSDL, wsdl_operation` |
171
171
  | Email (SMTP+IMAP) | messenger | `from tina4_python.messenger import Messenger` |
172
- | Background Queue | queue | `from tina4_python.queue import Queue, Producer, Consumer` |
172
+ | Background Queue | queue | `from tina4_python.queue import Queue` |
173
173
  | SCSS Compilation | scss | Auto-compiled from src/scss/ |
174
174
  | Migrations | migration | `tina4python migrate` CLI command |
175
175
  | Seeder | seeder | `from tina4_python.seeder import FakeData, seed_table` |
@@ -6,6 +6,10 @@ No PyJWT, no cryptography package.
6
6
  from tina4_python.auth import Auth
7
7
 
8
8
  auth = Auth(secret="my-secret")
9
+ token = auth.get_token({"user_id": 1, "role": "admin"})
10
+ payload = auth.valid_token(token)
11
+
12
+ # Legacy aliases also work:
9
13
  token = auth.create_token({"user_id": 1, "role": "admin"})
10
14
  payload = auth.validate_token(token)
11
15
 
@@ -34,7 +38,7 @@ class Auth:
34
38
 
35
39
  # ── JWT ────────────────────────────────────────────────────────
36
40
 
37
- def create_token(self, payload: dict, expiry_minutes: int = None) -> str:
41
+ def get_token(self, payload: dict, expiry_minutes: int = None) -> str:
38
42
  """Create a signed JWT token.
39
43
 
40
44
  Returns: header.payload.signature
@@ -53,7 +57,7 @@ class Auth:
53
57
 
54
58
  return f"{h}.{p}.{signature}"
55
59
 
56
- def validate_token(self, token: str) -> dict | None:
60
+ def valid_token(self, token: str) -> dict | None:
57
61
  """Validate a JWT and return the payload. None if invalid/expired."""
58
62
  try:
59
63
  parts = token.split(".")
@@ -86,12 +90,12 @@ class Auth:
86
90
 
87
91
  def refresh_token(self, token: str, expiry_minutes: int = None) -> str | None:
88
92
  """Validate and issue a fresh token with the same claims."""
89
- payload = self.validate_token(token)
93
+ payload = self.valid_token(token)
90
94
  if payload is None:
91
95
  return None
92
96
  payload.pop("iat", None)
93
97
  payload.pop("exp", None)
94
- return self.create_token(payload, expiry_minutes)
98
+ return self.get_token(payload, expiry_minutes)
95
99
 
96
100
  def _sign(self, message: str) -> str:
97
101
  sig = hmac.new(
@@ -99,6 +103,11 @@ class Auth:
99
103
  ).digest()
100
104
  return _b64url_encode(sig)
101
105
 
106
+ # ── Legacy aliases ─────────────────────────────────────────────
107
+
108
+ create_token = get_token
109
+ validate_token = valid_token
110
+
102
111
  # ── Password Hashing ──────────────────────────────────────────
103
112
 
104
113
  @staticmethod
@@ -149,7 +158,7 @@ class Auth:
149
158
 
150
159
  if auth_header.startswith("Bearer "):
151
160
  token = auth_header[7:]
152
- payload = self.validate_token(token)
161
+ payload = self.valid_token(token)
153
162
  if payload:
154
163
  return payload
155
164
  if self.validate_api_key(token):
@@ -7,6 +7,7 @@ CLI commands for development workflow.
7
7
  tina4python migrate # Run pending migrations
8
8
  tina4python migrate:create # Create a migration file
9
9
  tina4python migrate:rollback # Rollback last batch
10
+ tina4python migrate:status # Show completed and pending migrations
10
11
  tina4python seed # Run seeders
11
12
  tina4python routes # List registered routes
12
13
  tina4python test # Run tests
@@ -34,6 +35,7 @@ def main():
34
35
  "migrate": _migrate,
35
36
  "migrate:create": _migrate_create,
36
37
  "migrate:rollback": _migrate_rollback,
38
+ "migrate:status": _migrate_status,
37
39
  "seed": _seed,
38
40
  "routes": _routes,
39
41
  "test": _test,
@@ -63,6 +65,7 @@ Commands:
63
65
  migrate Run pending database migrations
64
66
  migrate:create <desc> Create a new migration file
65
67
  migrate:rollback Rollback last migration batch
68
+ migrate:status Show completed and pending migrations
66
69
  seed Run database seeders
67
70
  routes List all registered routes
68
71
  test Run test suite
@@ -233,7 +236,8 @@ def _migrate_rollback(args):
233
236
 
234
237
  db_url = os.environ.get("DATABASE_URL", "sqlite:///data/app.db")
235
238
  db = Database(db_url)
236
- rolled = rollback(db, "migrations")
239
+ mig_dir = args[0] if args else "migrations"
240
+ rolled = rollback(db, mig_dir)
237
241
  if rolled:
238
242
  for f in rolled:
239
243
  print(f" Rolled back: {f}")
@@ -243,6 +247,38 @@ def _migrate_rollback(args):
243
247
  db.close()
244
248
 
245
249
 
250
+ def _migrate_status(args):
251
+ """Show completed and pending migrations."""
252
+ _load_env()
253
+ from tina4_python.database import Database
254
+ from tina4_python.migration import status
255
+
256
+ db_url = os.environ.get("DATABASE_URL", "sqlite:///data/app.db")
257
+ db = Database(db_url)
258
+ mig_dir = args[0] if args else "migrations"
259
+ result = status(db, mig_dir)
260
+
261
+ completed = result["completed"]
262
+ pending = result["pending"]
263
+
264
+ if completed:
265
+ print("\nCompleted migrations:")
266
+ for m in completed:
267
+ print(f" [batch {m['batch']}] {m['migration_id']} ({m['executed_at']})")
268
+ else:
269
+ print("\nNo completed migrations.")
270
+
271
+ if pending:
272
+ print("\nPending migrations:")
273
+ for m in pending:
274
+ print(f" {m['migration_id']} ({m['description']})")
275
+ else:
276
+ print("\nNo pending migrations.")
277
+
278
+ print(f"\nTotal: {len(completed)} completed, {len(pending)} pending.")
279
+ db.close()
280
+
281
+
246
282
  def _seed(args):
247
283
  """Run seeders from src/seeds/."""
248
284
  _load_env()
@@ -14,7 +14,7 @@ from tina4_python.core.request import Request
14
14
  from tina4_python.core.response import Response
15
15
  from tina4_python.core.router import (
16
16
  Router, get, post, put, patch, delete, any_method,
17
- noauth, secured, middleware, cached,
17
+ noauth, secured, middleware, cached, websocket,
18
18
  )
19
19
  from tina4_python.core.middleware import CorsMiddleware, RateLimiter
20
20
  from tina4_python.core.cache import Cache
@@ -23,7 +23,7 @@ from tina4_python.core.server import run, resolve_config
23
23
 
24
24
  __all__ = [
25
25
  "Request", "Response", "Router",
26
- "get", "post", "put", "patch", "delete", "any_method",
26
+ "get", "post", "put", "patch", "delete", "any_method", "websocket",
27
27
  "noauth", "secured", "middleware", "cached",
28
28
  "CorsMiddleware", "RateLimiter",
29
29
  "Cache",
@@ -65,9 +65,14 @@ class Request:
65
65
 
66
66
  return req
67
67
 
68
+ def merge_route_params(self):
69
+ """Merge route params into params dict (route params take priority)."""
70
+ if self._route_params:
71
+ self.params.update(self._route_params)
72
+
68
73
  def param(self, key: str, default=None):
69
- """Get a route parameter (from URL path)."""
70
- return self._route_params.get(key, default)
74
+ """Get a route parameter (from URL path). Alias for params[key]."""
75
+ return self.params.get(key, self._route_params.get(key, default))
71
76
 
72
77
 
73
78
  def _extract_ip(scope: dict, headers: dict) -> str:
@@ -134,9 +139,10 @@ def _parse_multipart(body: bytes, content_type: str) -> dict:
134
139
  header_section = part[:header_end].decode(errors="replace")
135
140
  content = part[header_end + 4:].rstrip(b"\r\n")
136
141
 
137
- # Parse Content-Disposition
142
+ # Parse Content-Disposition and Content-Type
138
143
  name = None
139
144
  filename = None
145
+ file_type = "application/octet-stream"
140
146
  for line in header_section.split("\r\n"):
141
147
  if "Content-Disposition" in line:
142
148
  for token in line.split(";"):
@@ -145,15 +151,17 @@ def _parse_multipart(body: bytes, content_type: str) -> dict:
145
151
  name = token[5:].strip('"')
146
152
  elif token.startswith("filename="):
147
153
  filename = token[9:].strip('"')
154
+ elif "Content-Type" in line:
155
+ file_type = line.split(":", 1)[1].strip()
148
156
 
149
157
  if not name:
150
158
  continue
151
159
 
152
160
  if filename:
153
- import base64
154
161
  result[name] = {
155
- "file_name": filename,
156
- "content": base64.b64encode(content).decode(),
162
+ "filename": filename,
163
+ "type": file_type,
164
+ "content": bytes(content),
157
165
  "size": len(content),
158
166
  }
159
167
  else:
@@ -23,12 +23,146 @@ from tina4_python.debug import Log
23
23
  # Global route registry
24
24
  _routes: list[dict] = []
25
25
 
26
+ # Global WebSocket route registry
27
+ _ws_routes: list[dict] = []
28
+
29
+
30
+ class RouteRef:
31
+ """Thin wrapper around a registered route dict, enabling chained modifiers.
32
+
33
+ Usage::
34
+
35
+ Router.get("/api/data", handler).secure().cache()
36
+ """
37
+
38
+ __slots__ = ("_route",)
39
+
40
+ def __init__(self, route: dict):
41
+ self._route = route
42
+
43
+ def secure(self):
44
+ """Mark this route as requiring bearer-token authentication."""
45
+ self._route["auth_required"] = True
46
+ return self
47
+
48
+ def cache(self, max_age: int | None = None):
49
+ """Mark this route as cacheable.
50
+
51
+ Args:
52
+ max_age: Optional TTL override in seconds.
53
+ """
54
+ self._route["cached"] = True
55
+ if max_age is not None:
56
+ self._route["cache_max_age"] = max_age
57
+ return self
58
+
26
59
 
27
60
  class Router:
28
61
  """Route registry and matcher."""
29
62
 
63
+ # ── Group state (used by Router.group) ────────────────────────
64
+ _group_prefix: str = ""
65
+ _group_middleware: list = []
66
+
67
+ @classmethod
68
+ def group(cls, prefix: str, callback, middleware=None):
69
+ """Register routes with a shared prefix and optional middleware.
70
+
71
+ Saves/restores static prefix and middleware state around the
72
+ callback so that nested groups concatenate correctly.
73
+
74
+ Usage::
75
+
76
+ Router.group("/api", lambda: [
77
+ Router.get("/users", handler),
78
+ Router.post("/users", handler),
79
+ ], middleware=[auth_check])
80
+ """
81
+ prev_prefix = cls._group_prefix
82
+ prev_middleware = list(cls._group_middleware)
83
+
84
+ cls._group_prefix = prev_prefix + prefix.rstrip("/")
85
+ cls._group_middleware = prev_middleware + (middleware or [])
86
+
87
+ try:
88
+ callback()
89
+ finally:
90
+ cls._group_prefix = prev_prefix
91
+ cls._group_middleware = prev_middleware
92
+
93
+ @classmethod
94
+ def websocket(cls, path: str, handler) -> None:
95
+ """Register a WebSocket route (imperative, non-decorator style).
96
+
97
+ The handler signature is::
98
+
99
+ async def handler(connection, event, data):
100
+ ...
101
+
102
+ Where:
103
+ - ``connection`` is a :class:`WebSocketConnection`
104
+ - ``event`` is ``"open"``, ``"message"``, or ``"close"``
105
+ - ``data`` is the message payload (str for message, None for open/close)
106
+ """
107
+ pattern, param_names = _compile_pattern(path)
108
+ route = {
109
+ "path": path,
110
+ "pattern": pattern,
111
+ "param_names": param_names,
112
+ "handler": handler,
113
+ }
114
+ _ws_routes.append(route)
115
+ Log.debug(f"WebSocket route registered: {path}")
116
+
117
+ @staticmethod
118
+ def match_ws(path: str) -> tuple[dict | None, dict]:
119
+ """Find a WebSocket route matching the given path. Returns (route, params)."""
120
+ for route in _ws_routes:
121
+ m = route["pattern"].match(path)
122
+ if m:
123
+ params = {}
124
+ for i, name in enumerate(route["param_names"]):
125
+ params[name] = m.group(i + 1)
126
+ return route, params
127
+ return None, {}
128
+
30
129
  @staticmethod
31
- def add(method: str, path: str, handler, **options):
130
+ def all_ws() -> list[dict]:
131
+ """Return all registered WebSocket routes."""
132
+ return _ws_routes
133
+
134
+ @classmethod
135
+ def get(cls, path: str, handler, **options) -> "RouteRef":
136
+ """Register a GET route (imperative, non-decorator style)."""
137
+ return cls.add("GET", path, handler, **options)
138
+
139
+ @classmethod
140
+ def post(cls, path: str, handler, **options) -> "RouteRef":
141
+ """Register a POST route (imperative, non-decorator style)."""
142
+ return cls.add("POST", path, handler, **options)
143
+
144
+ @classmethod
145
+ def put(cls, path: str, handler, **options) -> "RouteRef":
146
+ """Register a PUT route (imperative, non-decorator style)."""
147
+ return cls.add("PUT", path, handler, **options)
148
+
149
+ @classmethod
150
+ def patch(cls, path: str, handler, **options) -> "RouteRef":
151
+ """Register a PATCH route (imperative, non-decorator style)."""
152
+ return cls.add("PATCH", path, handler, **options)
153
+
154
+ @classmethod
155
+ def delete(cls, path: str, handler, **options) -> "RouteRef":
156
+ """Register a DELETE route (imperative, non-decorator style)."""
157
+ return cls.add("DELETE", path, handler, **options)
158
+
159
+ @classmethod
160
+ def any(cls, path: str, handler, **options) -> "RouteRef":
161
+ """Register a route for any HTTP method (imperative, non-decorator style)."""
162
+ return cls.add("ANY", path, handler, **options)
163
+
164
+ @classmethod
165
+ def add(cls, method: str, path: str, handler, **options) -> "RouteRef":
32
166
  """Register a route handler.
33
167
 
34
168
  Auth defaults:
@@ -36,7 +170,21 @@ class Router:
36
170
  - POST/PUT/PATCH/DELETE require auth by default
37
171
  - Use @noauth() to make a write route public
38
172
  - Use @secured() to protect a GET route
173
+
174
+ Returns a :class:`RouteRef` so callers can chain ``.secure()`` /
175
+ ``.cache()``::
176
+
177
+ Router.get("/api/data", handler).secure().cache()
39
178
  """
179
+ # Apply group prefix
180
+ if cls._group_prefix:
181
+ path = cls._group_prefix + path
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)
187
+
40
188
  pattern, param_names = _compile_pattern(path)
41
189
 
42
190
  # Auth default: GET=public, writes=secured
@@ -50,7 +198,7 @@ class Router:
50
198
  else:
51
199
  auth_required = m not in ("GET", "ANY")
52
200
 
53
- _routes.append({
201
+ route = {
54
202
  "method": m,
55
203
  "path": path,
56
204
  "pattern": pattern,
@@ -60,8 +208,10 @@ class Router:
60
208
  "auth_required": auth_required,
61
209
  "cached": options.get("cached", False),
62
210
  "cache_max_age": options.get("cache_max_age", 60),
63
- })
211
+ }
212
+ _routes.append(route)
64
213
  Log.debug(f"Route registered: {m} {path} (auth={'required' if auth_required else 'public'})")
214
+ return RouteRef(route)
65
215
 
66
216
  @staticmethod
67
217
  def match(method: str, path: str) -> tuple[dict | None, dict]:
@@ -87,6 +237,7 @@ class Router:
87
237
  def clear():
88
238
  """Clear all routes (for testing)."""
89
239
  _routes.clear()
240
+ _ws_routes.clear()
90
241
 
91
242
 
92
243
  def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
@@ -173,6 +324,27 @@ def any_method(path: str, **options):
173
324
  return fn
174
325
  return decorator
175
326
 
327
+ # Alias — @any() is the standard name across all Tina4 frameworks
328
+ any = any_method
329
+
330
+
331
+ def websocket(path: str):
332
+ """Register a WebSocket route.
333
+
334
+ Usage::
335
+
336
+ @websocket("/ws/chat/{room}")
337
+ async def chat(connection, event, data):
338
+ if event == "message":
339
+ await connection.broadcast(data)
340
+ elif event == "open":
341
+ await connection.send(f"Welcome to {connection.params['room']}")
342
+ """
343
+ def decorator(fn):
344
+ Router.websocket(path, fn)
345
+ return fn
346
+ return decorator
347
+
176
348
 
177
349
  # ── Auth Decorators ────────────────────────────────────────────
178
350