tina4-python 3.13.1__tar.gz → 3.13.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. {tina4_python-3.13.1 → tina4_python-3.13.3}/PKG-INFO +1 -1
  2. {tina4_python-3.13.1 → tina4_python-3.13.3}/pyproject.toml +1 -1
  3. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/__init__.py +4 -1
  4. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/connection.py +3 -2
  5. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/debug/__init__.py +51 -0
  6. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/dev_admin/__init__.py +8 -4
  7. tina4_python-3.13.3/tina4_python/env.py +111 -0
  8. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/orm/model.py +2 -2
  9. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/__init__.py +65 -27
  10. {tina4_python-3.13.1 → tina4_python-3.13.3}/.gitignore +0 -0
  11. {tina4_python-3.13.1 → tina4_python-3.13.3}/README.md +0 -0
  12. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/CLAUDE.md +0 -0
  13. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/HtmlElement.py +0 -0
  14. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/Testing.py +0 -0
  15. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/ai/__init__.py +0 -0
  16. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/api/__init__.py +0 -0
  17. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/auth/__init__.py +0 -0
  18. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/cache/__init__.py +0 -0
  19. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/cli/__init__.py +0 -0
  20. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/container/__init__.py +0 -0
  21. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/firebird.py +0 -0
  35. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/mongodb.py +0 -0
  36. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/mssql.py +0 -0
  37. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/mysql.py +0 -0
  38. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/odbc.py +0 -0
  39. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/postgres.py +0 -0
  40. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/database/sqlite.py +0 -0
  41. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/debug/error_overlay.py +0 -0
  42. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/orm/__init__.py +0 -0
  75. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/orm/fields.py +0 -0
  76. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/__feedback/widget.js +0 -0
  77. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/css/tina4.css +0 -0
  78. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/css/tina4.min.css +0 -0
  79. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/favicon.ico +0 -0
  80. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/images/logo.svg +0 -0
  81. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  82. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/frond.js +0 -0
  83. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  85. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  86. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.1 → tina4_python-3.13.3}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.1 → tina4_python-3.13.3}/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.13.1
3
+ Version: 3.13.3
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.13.1"
3
+ version = "3.13.3"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.1"
11
+ __version__ = "3.13.3"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -42,6 +42,9 @@ from tina4_python.orm import ( # noqa: E402, F401
42
42
  has_many, has_one, belongs_to, # relationship descriptors
43
43
  )
44
44
 
45
+ # ── Env (typed env-var helpers) ──
46
+ from tina4_python.env import Env # noqa: E402, F401
47
+
45
48
  # ── Auth ──
46
49
  from tina4_python.auth import Auth # noqa: E402, F401
47
50
 
@@ -725,8 +725,9 @@ class Database:
725
725
  def from_env(env_key: str = "TINA4_DATABASE_URL", pool: int = 0) -> "Database | None":
726
726
  """Construct a Database instance from environment variables.
727
727
 
728
- Reads the connection URL from the named env var (default DATABASE_URL),
729
- and DATABASE_USERNAME / DATABASE_PASSWORD for credentials.
728
+ Reads the connection URL from the named env var (default
729
+ TINA4_DATABASE_URL), and TINA4_DATABASE_USERNAME /
730
+ TINA4_DATABASE_PASSWORD for credentials.
730
731
 
731
732
  Args:
732
733
  env_key: Environment variable name holding the connection URL.
@@ -320,10 +320,57 @@ class Log:
320
320
  now = datetime.now(timezone.utc)
321
321
  return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z"
322
322
 
323
+ # Frame names that belong to Log itself — walk past them when looking
324
+ # for the real caller. Kept as a class attribute so subclasses can
325
+ # extend if they layer extra wrappers (and so tests can introspect).
326
+ _OWN_FRAMES = frozenset({
327
+ "_caller_name", "_format_line", "_format", "_log",
328
+ "debug", "info", "warning", "error", "critical",
329
+ })
330
+
331
+ @classmethod
332
+ def _caller_name(cls) -> str | None:
333
+ """Return the function name that called Log.{debug,info,warning,error}.
334
+
335
+ Active only when ``TINA4_LOG_FUNC=true`` — the lookup uses
336
+ ``inspect.currentframe()`` which is ~5% overhead per log call.
337
+ Walks past Log's own frames (``_OWN_FRAMES``) to land on the
338
+ real caller, so the count is robust whether the test calls
339
+ ``_format_line`` directly or goes through ``Log.info`` →
340
+ ``_log`` → ``_format_line``. Returns ``None`` if the stack is
341
+ too shallow (e.g. log called from module import) or if anything
342
+ goes wrong — never raises. Parity feature #41 across all four
343
+ Tina4 frameworks.
344
+ """
345
+ import os
346
+ if os.environ.get("TINA4_LOG_FUNC", "").strip().lower() not in (
347
+ "1", "true", "on", "yes", "y", "t"
348
+ ):
349
+ return None
350
+ try:
351
+ import inspect
352
+ frame = inspect.currentframe()
353
+ # Walk past Log's own frames. Cap the walk at 16 to defend
354
+ # against any pathological recursion / wrapper stack.
355
+ for _ in range(16):
356
+ if frame is None:
357
+ return None
358
+ name = frame.f_code.co_name
359
+ if name not in cls._OWN_FRAMES:
360
+ # Filter out anonymous / module-level callers — they're noise.
361
+ if name in ("<module>", "<lambda>", "<genexpr>", "<listcomp>", "<setcomp>", "<dictcomp>"):
362
+ return None
363
+ return name
364
+ frame = frame.f_back
365
+ return None
366
+ except Exception:
367
+ return None
368
+
323
369
  @classmethod
324
370
  def _format_line(cls, level: str, message: str, **kwargs) -> str:
325
371
  timestamp = cls._timestamp()
326
372
  request_id = get_request_id()
373
+ caller = cls._caller_name()
327
374
 
328
375
  # JSON format wins whenever the explicit env opt-in is set,
329
376
  # regardless of production flag (so dev devs can ship JSON to
@@ -336,6 +383,8 @@ class Log:
336
383
  }
337
384
  if request_id:
338
385
  entry["request_id"] = request_id
386
+ if caller:
387
+ entry["function"] = caller
339
388
  if kwargs:
340
389
  entry["context"] = {k: v for k, v in kwargs.items()}
341
390
  return json.dumps(entry, default=str)
@@ -345,6 +394,8 @@ class Log:
345
394
  parts = [timestamp, f"[{level_str}]"]
346
395
  if request_id:
347
396
  parts.append(f"[{request_id}]")
397
+ if caller:
398
+ parts.append(f"[{caller}]")
348
399
  parts.append(message)
349
400
  if kwargs:
350
401
  parts.append(json.dumps(kwargs, default=str))
@@ -1743,7 +1743,7 @@ def _tina4_robot_fallback(message: str) -> str:
1743
1743
  elif "orm" in msg or "model" in msg:
1744
1744
  return "Define ORM models in src/orm/ — one class per file. Use IntegerField, StringField, etc. Call model.save(), model.load(), model.select(). Don't forget to create a migration for the table."
1745
1745
  elif "database" in msg or "db" in msg:
1746
- return "Set DATABASE_URL in .env. Supports sqlite, postgres, mysql, firebird, mssql, mongodb. Use db.fetch(), db.insert(), db.update(), db.delete()."
1746
+ return "Set TINA4_DATABASE_URL in .env. Supports sqlite, postgres, mysql, firebird, mssql, mongodb. Use db.fetch(), db.insert(), db.update(), db.delete()."
1747
1747
  elif "queue" in msg:
1748
1748
  return "Use Queue(topic='name') with queue.produce() to enqueue, queue.consume() to process. Supports litequeue, RabbitMQ, Kafka, MongoDB backends."
1749
1749
  elif "template" in msg or "twig" in msg:
@@ -1848,14 +1848,18 @@ async def _api_connections_save(request, response):
1848
1848
  new_lines.append(line)
1849
1849
  continue
1850
1850
  key = stripped.split("=", 1)[0].strip()
1851
+ # Preserve the TINA4_ prefix when rewriting — the boot guard
1852
+ # rejects bare DATABASE_URL / DATABASE_USERNAME / DATABASE_PASSWORD
1853
+ # since v3.12, so stripping the prefix here would break the next
1854
+ # restart. Bug filed as tina4-python#45.
1851
1855
  if key == "TINA4_DATABASE_URL":
1852
- new_lines.append(f"DATABASE_URL={url}")
1856
+ new_lines.append(f"TINA4_DATABASE_URL={url}")
1853
1857
  keys_found["TINA4_DATABASE_URL"] = True
1854
1858
  elif key == "TINA4_DATABASE_USERNAME":
1855
- new_lines.append(f"DATABASE_USERNAME={username}")
1859
+ new_lines.append(f"TINA4_DATABASE_USERNAME={username}")
1856
1860
  keys_found["TINA4_DATABASE_USERNAME"] = True
1857
1861
  elif key == "TINA4_DATABASE_PASSWORD":
1858
- new_lines.append(f"DATABASE_PASSWORD={password}")
1862
+ new_lines.append(f"TINA4_DATABASE_PASSWORD={password}")
1859
1863
  keys_found["TINA4_DATABASE_PASSWORD"] = True
1860
1864
  else:
1861
1865
  new_lines.append(line)
@@ -0,0 +1,111 @@
1
+ # Tina4 Env — Typed environment-variable helpers.
2
+ # Copyright 2007 - present Tina4
3
+ # License: MIT
4
+ """Typed environment-variable helpers — zero-deps.
5
+
6
+ Reading env vars by hand gets old fast: every boolean flag becomes a
7
+ ``os.getenv("TINA4_DEBUG", "false").lower() in ("1", "true", "on", "yes")``
8
+ incantation. Every numeric tuning knob needs a try/except around int().
9
+ Env centralises that. Same API across all four Tina4 frameworks
10
+ (``Tina4\\Env`` in PHP, ``Tina4::Env`` in Ruby, ``Env`` in Node.js).
11
+
12
+ from tina4_python import Env
13
+
14
+ debug = Env.bool("TINA4_DEBUG", default=False)
15
+ workers = Env.int("WORKERS", default=4)
16
+ rate = Env.float("RATE_LIMIT", default=10.0)
17
+ region = Env.str("AWS_REGION", default="us-east-1")
18
+
19
+ Values are accepted case-insensitively after ``strip()``. Truthy:
20
+ ``1 / true / on / yes / y / t``. Falsy: ``0 / false / off / no / n / f``.
21
+ Any other value triggers the ``default``. Unparseable ints and floats
22
+ log a warning via ``Log`` and fall back to ``default`` — never raise.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+
28
+ # Lazy import so ``import tina4_python.env`` doesn't drag the whole Log
29
+ # chain in for callers who don't want it (CLI scaffolding, etc).
30
+ _TRUTHY = frozenset({"1", "true", "on", "yes", "y", "t"})
31
+ _FALSY = frozenset({"0", "false", "off", "no", "n", "f", ""})
32
+
33
+
34
+ def _log_warning(message: str) -> None:
35
+ """Emit a warning via Log without creating a circular import at module load."""
36
+ try:
37
+ from tina4_python.debug import Log
38
+ Log.warning(message)
39
+ except Exception:
40
+ # Log not yet wired (e.g. very early bootstrap) — silently skip.
41
+ pass
42
+
43
+
44
+ class Env:
45
+ """Typed environment-variable helpers.
46
+
47
+ All methods are classmethods so callers can write ``Env.bool(...)``
48
+ without instantiating anything — matching the PHP/Ruby/Node ports.
49
+ """
50
+
51
+ @classmethod
52
+ def bool(cls, name: str, default: bool = False) -> bool:
53
+ """Read ``name`` and coerce to bool.
54
+
55
+ Truthy values (case-insensitive after strip): ``1``, ``true``,
56
+ ``on``, ``yes``, ``y``, ``t``. Falsy: ``0``, ``false``, ``off``,
57
+ ``no``, ``n``, ``f``, empty string. Anything else returns the
58
+ ``default`` — never raises.
59
+ """
60
+ raw = os.environ.get(name)
61
+ if raw is None:
62
+ return default
63
+ token = raw.strip().lower()
64
+ if token in _TRUTHY:
65
+ return True
66
+ if token in _FALSY:
67
+ return False
68
+ return default
69
+
70
+ @classmethod
71
+ def int(cls, name: str, default: int = 0) -> int:
72
+ """Read ``name`` and coerce to int. Returns ``default`` on parse failure."""
73
+ raw = os.environ.get(name)
74
+ if raw is None:
75
+ return default
76
+ try:
77
+ return int(raw.strip())
78
+ except (ValueError, TypeError):
79
+ _log_warning(
80
+ f"Env.int({name!r}): could not parse {raw!r} as int — "
81
+ f"using default {default!r}"
82
+ )
83
+ return default
84
+
85
+ @classmethod
86
+ def float(cls, name: str, default: float = 0.0) -> float:
87
+ """Read ``name`` and coerce to float. Returns ``default`` on parse failure."""
88
+ raw = os.environ.get(name)
89
+ if raw is None:
90
+ return default
91
+ try:
92
+ return float(raw.strip())
93
+ except (ValueError, TypeError):
94
+ _log_warning(
95
+ f"Env.float({name!r}): could not parse {raw!r} as float — "
96
+ f"using default {default!r}"
97
+ )
98
+ return default
99
+
100
+ @classmethod
101
+ def str(cls, name: str, default: str = "") -> str:
102
+ """Read ``name`` as a string. Returns ``default`` if unset.
103
+
104
+ Whitespace is preserved — this is a pass-through for the raw env
105
+ value. ``Env.str("PATH")`` is exactly ``os.environ.get("PATH", "")``
106
+ with a more discoverable name.
107
+ """
108
+ raw = os.environ.get(name)
109
+ if raw is None:
110
+ return default
111
+ return raw
@@ -278,7 +278,7 @@ class ORM(metaclass=ORMMeta):
278
278
  return cls._db # Direct Database instance
279
279
 
280
280
  if _database is None:
281
- # Try auto-discovery from DATABASE_URL
281
+ # Try auto-discovery from TINA4_DATABASE_URL
282
282
  import os
283
283
  url = os.environ.get("TINA4_DATABASE_URL")
284
284
  if url:
@@ -289,7 +289,7 @@ class ORM(metaclass=ORMMeta):
289
289
  orm_bind(db)
290
290
  return db
291
291
  raise RuntimeError(
292
- "No database bound. Call orm_bind(db) or set DATABASE_URL in .env"
292
+ "No database bound. Call orm_bind(db) or set TINA4_DATABASE_URL in .env"
293
293
  )
294
294
  return _database
295
295
 
@@ -249,41 +249,79 @@ def _resolve_extends(scss: str, placeholders: dict) -> str:
249
249
 
250
250
 
251
251
  def _eval_math(scss: str) -> str:
252
- """Evaluate simple math in property values (e.g., 10px + 5px)."""
252
+ """Evaluate simple math in property values (e.g., 10px + 5px).
253
+
254
+ Mixed-unit arithmetic is left verbatim — that is exactly what CSS calc()
255
+ is for, and folding it silently produces invalid output (see #42). Math
256
+ inside calc(...) is preserved untouched on the same principle: the author
257
+ asked the browser to compute it.
258
+
259
+ Rules for folding:
260
+ * Both operands unitless → fold
261
+ * Same unit on both operands → fold, keep unit
262
+ * One operand unitless for * or / → fold, keep the other unit
263
+ * Anything else (mixed units on +/-, etc) → leave verbatim
264
+ """
265
+ # Step 1 — mask calc(...) ranges so the math regex cannot eat into them.
266
+ # CSS calc() is the contract for mixed-unit arithmetic; the browser does it.
267
+ placeholders: list[str] = []
268
+
269
+ def _mask(m):
270
+ placeholders.append(m.group(0))
271
+ return f"\x00CALC{len(placeholders) - 1}\x00"
272
+
273
+ masked = re.sub(r'calc\([^()]*\)', _mask, scss)
274
+
275
+ # Step 2 — run the math fold on what remains.
253
276
  def _calc(m):
254
277
  full = m.group(0)
255
278
  try:
256
- # Extract numbers and operator
257
- num1 = float(re.search(r'[\d.]+', m.group(1)).group())
258
- num2 = float(re.search(r'[\d.]+', m.group(3)).group())
259
- op = m.group(2).strip()
260
- # Extract unit from first operand
261
- unit_match = re.search(r'[a-z%]+', m.group(1))
262
- unit = unit_match.group() if unit_match else ""
263
-
264
- if op == "+":
265
- result = num1 + num2
266
- elif op == "-":
267
- result = num1 - num2
268
- elif op == "*":
269
- result = num1 * num2
270
- elif op == "/":
271
- result = num1 / num2 if num2 != 0 else 0
272
- else:
273
- return full
274
-
275
- # Format result
276
- if result == int(result):
277
- return f"{int(result)}{unit}"
278
- return f"{result:.2f}{unit}"
279
+ num1 = float(m.group(1))
280
+ unit1 = m.group(2) or ""
281
+ op = m.group(3)
282
+ num2 = float(m.group(4))
283
+ unit2 = m.group(5) or ""
279
284
  except (ValueError, AttributeError):
280
285
  return full
281
286
 
282
- return re.sub(
283
- r'([\d.]+[a-z%]*)\s*([+\-*/])\s*([\d.]+[a-z%]*)',
284
- _calc, scss
287
+ # Decide result unit; bail (leave verbatim) if units are incompatible.
288
+ if unit1 == unit2:
289
+ unit = unit1
290
+ elif op in ("*", "/") and not unit1:
291
+ unit = unit2
292
+ elif op in ("*", "/") and not unit2:
293
+ unit = unit1
294
+ else:
295
+ return full
296
+
297
+ if op == "+":
298
+ result = num1 + num2
299
+ elif op == "-":
300
+ result = num1 - num2
301
+ elif op == "*":
302
+ result = num1 * num2
303
+ elif op == "/":
304
+ if num2 == 0:
305
+ return full
306
+ result = num1 / num2
307
+ else:
308
+ return full
309
+
310
+ if result == int(result):
311
+ return f"{int(result)}{unit}"
312
+ return f"{result:.2f}{unit}"
313
+
314
+ folded = re.sub(
315
+ r'([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)',
316
+ _calc, masked
285
317
  )
286
318
 
319
+ # Step 3 — restore the calc() ranges verbatim.
320
+ def _unmask(m):
321
+ return placeholders[int(m.group(1))]
322
+
323
+ return re.sub(r'\x00CALC(\d+)\x00', _unmask, folded)
324
+
287
325
 
288
326
  def _resolve_color_functions(scss: str, variables: dict) -> str:
289
327
  """Resolve lighten(), darken(), rgba() functions."""
File without changes
File without changes