tina4-python 3.13.1__tar.gz → 3.13.2__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 (156) hide show
  1. {tina4_python-3.13.1 → tina4_python-3.13.2}/PKG-INFO +1 -1
  2. {tina4_python-3.13.1 → tina4_python-3.13.2}/pyproject.toml +1 -1
  3. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/connection.py +3 -2
  5. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/dev_admin/__init__.py +8 -4
  6. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/orm/model.py +2 -2
  7. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/__init__.py +65 -27
  8. {tina4_python-3.13.1 → tina4_python-3.13.2}/.gitignore +0 -0
  9. {tina4_python-3.13.1 → tina4_python-3.13.2}/README.md +0 -0
  10. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/CLAUDE.md +0 -0
  11. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/ai/__init__.py +0 -0
  14. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/cli/__init__.py +0 -0
  18. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/rate_limiter.py +0 -0
  25. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/core/server.py +0 -0
  29. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/crud/__init__.py +0 -0
  30. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/adapter.py +0 -0
  32. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/frond/FROND.md +0 -0
  47. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/frond/__init__.py +0 -0
  48. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/frond/engine.py +0 -0
  49. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/auth/meta.json +0 -0
  50. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  51. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/database/meta.json +0 -0
  52. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  53. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/error-overlay/meta.json +0 -0
  54. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  55. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/orm/meta.json +0 -0
  56. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  57. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  58. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/queue/meta.json +0 -0
  59. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  60. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/rest-api/meta.json +0 -0
  61. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  62. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/templates/meta.json +0 -0
  63. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  64. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  65. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/graphql/__init__.py +0 -0
  66. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/i18n/__init__.py +0 -0
  67. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/mcp/__init__.py +0 -0
  68. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/mcp/protocol.py +0 -0
  69. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/mcp/tools.py +0 -0
  70. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  101. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_badges.scss +0 -0
  102. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  103. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_cards.scss +0 -0
  104. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_forms.scss +0 -0
  105. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_grid.scss +0 -0
  106. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_modals.scss +0 -0
  107. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_nav.scss +0 -0
  108. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_reset.scss +0 -0
  109. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_tables.scss +0 -0
  110. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_typography.scss +0 -0
  111. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  112. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/_variables.scss +0 -0
  113. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/base.scss +0 -0
  114. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/colors.scss +0 -0
  115. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/scss/tina4css/tina4.scss +0 -0
  116. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/seeder/__init__.py +0 -0
  117. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/service/__init__.py +0 -0
  118. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/session/__init__.py +0 -0
  119. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/session_handlers/__init__.py +0 -0
  120. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  121. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/session_handlers/redis_handler.py +0 -0
  122. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/session_handlers/valkey_handler.py +0 -0
  123. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/swagger/__init__.py +0 -0
  124. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/components/crud.twig +0 -0
  125. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  126. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  127. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/docker/python/Dockerfile +0 -0
  128. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  129. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/302.twig +0 -0
  130. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/401.twig +0 -0
  131. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/403.twig +0 -0
  132. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/404.twig +0 -0
  133. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/500.twig +0 -0
  134. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/502.twig +0 -0
  135. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/503.twig +0 -0
  136. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/errors/base.twig +0 -0
  137. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/frontend/README.md +0 -0
  138. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/templates/readme.md +0 -0
  139. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/test/__init__.py +0 -0
  140. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/test_client/__init__.py +0 -0
  141. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/validator/__init__.py +0 -0
  154. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/websocket/__init__.py +0 -0
  155. {tina4_python-3.13.1 → tina4_python-3.13.2}/tina4_python/websocket/backplane.py +0 -0
  156. {tina4_python-3.13.1 → tina4_python-3.13.2}/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.2
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.2"
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.2"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -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.
@@ -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)
@@ -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