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