tina4-python 3.10.48__tar.gz → 3.10.50__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 (143) hide show
  1. {tina4_python-3.10.48 → tina4_python-3.10.50}/PKG-INFO +1 -1
  2. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/__init__.py +23 -1
  3. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/cli/__init__.py +18 -0
  4. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/request.py +15 -0
  5. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/router.py +1 -1
  6. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/server.py +4 -0
  7. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/adapter.py +23 -11
  8. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/connection.py +1 -1
  9. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/firebird.py +1 -1
  10. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/mssql.py +1 -1
  11. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/mysql.py +1 -1
  12. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/odbc.py +1 -1
  13. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/postgres.py +1 -1
  14. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/sqlite.py +1 -1
  15. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/engine.py +69 -0
  16. {tina4_python-3.10.48 → tina4_python-3.10.50}/.gitignore +0 -0
  17. {tina4_python-3.10.48 → tina4_python-3.10.50}/README.md +0 -0
  18. {tina4_python-3.10.48 → tina4_python-3.10.50}/pyproject.toml +0 -0
  19. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/CLAUDE.md +0 -0
  20. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/HtmlElement.py +0 -0
  21. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/Testing.py +0 -0
  22. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/ai/__init__.py +0 -0
  23. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/api/__init__.py +0 -0
  24. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/auth/__init__.py +0 -0
  25. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/cache/__init__.py +0 -0
  26. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/container/__init__.py +0 -0
  27. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/__init__.py +0 -0
  28. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/cache.py +0 -0
  29. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/constants.py +0 -0
  30. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/events.py +0 -0
  31. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/middleware.py +0 -0
  32. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/response.py +0 -0
  33. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/crud/__init__.py +0 -0
  34. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/__init__.py +0 -0
  35. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/debug/__init__.py +0 -0
  36. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/debug/error_overlay.py +0 -0
  37. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_admin/__init__.py +0 -0
  38. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_admin/metrics.py +0 -0
  39. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_reload.py +0 -0
  40. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/auth/meta.json +0 -0
  44. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  45. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/database/meta.json +0 -0
  46. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  47. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/error-overlay/meta.json +0 -0
  48. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  49. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/meta.json +0 -0
  50. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  51. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  52. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/queue/meta.json +0 -0
  53. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  54. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/rest-api/meta.json +0 -0
  55. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  56. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/meta.json +0 -0
  57. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  58. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  59. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/graphql/__init__.py +0 -0
  60. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/i18n/__init__.py +0 -0
  61. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/__init__.py +0 -0
  62. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/protocol.py +0 -0
  63. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/tools.py +0 -0
  64. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/messenger/__init__.py +0 -0
  65. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/migration/__init__.py +0 -0
  66. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/migration/runner.py +0 -0
  67. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/__init__.py +0 -0
  68. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/fields.py +0 -0
  69. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/model.py +0 -0
  70. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/css/tina4.css +0 -0
  71. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/css/tina4.min.css +0 -0
  72. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/favicon.ico +0 -0
  73. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/images/logo.svg +0 -0
  74. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  75. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/frond.min.js +0 -0
  76. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  77. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4.min.js +0 -0
  78. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4js.min.js +0 -0
  79. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/swagger/index.html +0 -0
  80. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  81. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/query_builder/__init__.py +0 -0
  82. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue/__init__.py +0 -0
  83. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/__init__.py +0 -0
  84. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/kafka_backend.py +0 -0
  85. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/mongo_backend.py +0 -0
  86. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  87. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/__init__.py +0 -0
  88. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  89. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_badges.scss +0 -0
  90. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  91. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_cards.scss +0 -0
  92. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_forms.scss +0 -0
  93. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_grid.scss +0 -0
  94. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_modals.scss +0 -0
  95. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_nav.scss +0 -0
  96. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_reset.scss +0 -0
  97. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_tables.scss +0 -0
  98. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_typography.scss +0 -0
  99. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  100. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_variables.scss +0 -0
  101. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/base.scss +0 -0
  102. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/colors.scss +0 -0
  103. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/tina4.scss +0 -0
  104. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/seeder/__init__.py +0 -0
  105. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/service/__init__.py +0 -0
  106. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session/__init__.py +0 -0
  107. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/__init__.py +0 -0
  108. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  109. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/redis_handler.py +0 -0
  110. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/valkey_handler.py +0 -0
  111. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/swagger/__init__.py +0 -0
  112. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/components/crud.twig +0 -0
  113. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  114. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  115. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/python/Dockerfile +0 -0
  116. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  117. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/302.twig +0 -0
  118. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/401.twig +0 -0
  119. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/403.twig +0 -0
  120. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/404.twig +0 -0
  121. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/500.twig +0 -0
  122. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/502.twig +0 -0
  123. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/503.twig +0 -0
  124. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/base.twig +0 -0
  125. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/frontend/README.md +0 -0
  126. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/readme.md +0 -0
  127. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/test_client/__init__.py +0 -0
  128. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  137. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  138. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/validator/__init__.py +0 -0
  141. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/websocket/__init__.py +0 -0
  142. {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/websocket/backplane.py +0 -0
  143. {tina4_python-3.10.48 → tina4_python-3.10.50}/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.10.48
3
+ Version: 3.10.50
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -8,7 +8,14 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.10.48"
11
+ __version__ = "3.10.50"
12
+
13
+ # ── Route decorators ──
14
+ from tina4_python.core.router import ( # noqa: E402, F401
15
+ get, post, put, patch, delete, any_method,
16
+ noauth, secured, cached, middleware, template,
17
+ Router,
18
+ )
12
19
 
13
20
  # ── HTTP Constants ──
14
21
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -22,6 +29,9 @@ from tina4_python.core.constants import ( # noqa: E402, F401
22
29
  APPLICATION_OCTET, TEXT_HTML, TEXT_PLAIN, TEXT_CSV, TEXT_XML,
23
30
  )
24
31
 
32
+ # ── Database ──
33
+ from tina4_python.database import Database # noqa: E402, F401
34
+
25
35
  # ── ORM ──
26
36
  from tina4_python.orm import ( # noqa: E402, F401
27
37
  ORM, orm_bind, Field,
@@ -31,6 +41,15 @@ from tina4_python.orm import ( # noqa: E402, F401
31
41
  has_many, has_one, belongs_to, # relationship descriptors
32
42
  )
33
43
 
44
+ # ── Auth ──
45
+ from tina4_python.auth import Auth # noqa: E402, F401
46
+
47
+ # ── Queue ──
48
+ from tina4_python.queue import Queue # noqa: E402, F401
49
+
50
+ # ── Template engine ──
51
+ from tina4_python.frond import Frond # noqa: E402, F401
52
+
34
53
  # ── Response Cache ──
35
54
  from tina4_python.cache import ( # noqa: E402, F401
36
55
  ResponseCache, cache_stats, clear_cache,
@@ -38,3 +57,6 @@ from tina4_python.cache import ( # noqa: E402, F401
38
57
 
39
58
  # ── DI Container ──
40
59
  from tina4_python.container import Container # noqa: E402, F401
60
+
61
+ # ── Server ──
62
+ from tina4_python.core.server import run # noqa: E402, F401
@@ -202,6 +202,24 @@ def _init(args):
202
202
  for folder in folders:
203
203
  (target / folder).mkdir(parents=True, exist_ok=True)
204
204
 
205
+ # Copy framework public assets into the project so they're visible
206
+ framework_public = Path(__file__).parent.parent / "public"
207
+ project_public = target / "src" / "public"
208
+ assets_to_copy = [
209
+ "css/tina4.css",
210
+ "css/tina4.min.css",
211
+ "js/tina4.min.js",
212
+ "js/frond.min.js",
213
+ "images/tina4-logo-icon.webp",
214
+ ]
215
+ for asset in assets_to_copy:
216
+ src = framework_public / asset
217
+ dst = project_public / asset
218
+ dst.parent.mkdir(parents=True, exist_ok=True)
219
+ if src.exists() and not dst.exists():
220
+ import shutil
221
+ shutil.copy2(src, dst)
222
+
205
223
  # Copy frontend README
206
224
  frontend_readme = target / "frontend" / "README.md"
207
225
  if not frontend_readme.exists():
@@ -80,6 +80,21 @@ class Request:
80
80
  # Parse body
81
81
  req.body = _parse_body(body, req.content_type)
82
82
 
83
+ # Separate files from body for multipart uploads
84
+ if isinstance(req.body, dict) and "multipart/form-data" in req.content_type:
85
+ files = {}
86
+ fields = {}
87
+ for key, value in req.body.items():
88
+ if isinstance(value, dict) and "filename" in value:
89
+ # Base64-encode file content for safe transport
90
+ import base64
91
+ value["content"] = base64.b64encode(value["content"]).decode()
92
+ files[key] = value
93
+ else:
94
+ fields[key] = value
95
+ req.files = files
96
+ req.body = fields
97
+
83
98
  return req
84
99
 
85
100
  def merge_route_params(self):
@@ -262,7 +262,7 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
262
262
  for i, segment in enumerate(segments):
263
263
  if segment == "*":
264
264
  # Wildcard: matches the rest of the path (greedy)
265
- param_names.append("wildcard")
265
+ param_names.append("*")
266
266
  regex_parts.append("(.+)")
267
267
  break # Nothing can follow a wildcard
268
268
  elif segment.startswith("{") and segment.endswith("}"):
@@ -640,6 +640,10 @@ def _init_session(request: Request) -> None:
640
640
  sess = Session()
641
641
  sess.start(sid_match)
642
642
  request.session = sess
643
+ # Probabilistic garbage collection (1% of requests)
644
+ import random
645
+ if random.randint(1, 100) == 1:
646
+ sess.gc()
643
647
  except Exception:
644
648
  pass # Session module not available — session stays None
645
649
 
@@ -33,8 +33,10 @@ class DatabaseResult:
33
33
 
34
34
  def to_paginate(self, page: int = 1, per_page: int = 20) -> dict:
35
35
  total_pages = max(1, -(-self.count // per_page)) # ceil division
36
+ start = (page - 1) * per_page
37
+ end = start + per_page
36
38
  return {
37
- "data": self.records,
39
+ "data": self.records[start:end],
38
40
  "total": self.count,
39
41
  "page": page,
40
42
  "per_page": per_page,
@@ -210,21 +212,31 @@ class DatabaseResult:
210
212
  return columns
211
213
 
212
214
  def _fallback_column_info(self) -> list[dict]:
213
- """Derive basic column info from record keys when no adapter is available."""
215
+ """Derive basic column info from record keys and values when no adapter is available."""
214
216
  if not self.records:
215
217
  return []
216
- keys = list(self.records[0].keys()) if isinstance(self.records[0], dict) else []
217
- return [
218
- {
218
+ row = self.records[0] if isinstance(self.records[0], dict) else {}
219
+ result = []
220
+ for k, v in row.items():
221
+ if isinstance(v, int):
222
+ col_type = "INTEGER"
223
+ elif isinstance(v, float):
224
+ col_type = "REAL"
225
+ elif isinstance(v, bool):
226
+ col_type = "BOOLEAN"
227
+ elif v is None:
228
+ col_type = "TEXT"
229
+ else:
230
+ col_type = "TEXT"
231
+ result.append({
219
232
  "name": k,
220
- "type": "UNKNOWN",
233
+ "type": col_type,
221
234
  "size": None,
222
235
  "decimals": None,
223
236
  "nullable": True,
224
- "primary_key": False,
225
- }
226
- for k in keys
227
- ]
237
+ "primary_key": k.lower() == "id",
238
+ })
239
+ return result
228
240
 
229
241
 
230
242
  class DatabaseAdapter:
@@ -285,7 +297,7 @@ class DatabaseAdapter:
285
297
  )
286
298
 
287
299
  def fetch(self, sql: str, params: list = None,
288
- limit: int = 20, offset: int = 0) -> DatabaseResult:
300
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
289
301
  """Execute a read query and return multiple rows."""
290
302
  raise NotImplementedError
291
303
 
@@ -283,7 +283,7 @@ class Database:
283
283
  return adapter.execute_many(sql, params_list)
284
284
 
285
285
  def fetch(self, sql: str, params: list = None,
286
- limit: int = 20, offset: int = 0) -> DatabaseResult:
286
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
287
287
  """Fetch rows with pagination."""
288
288
  if self._cache_enabled:
289
289
  key = self._cache_key(sql + f":L{limit}:S{offset}", params)
@@ -138,7 +138,7 @@ class FirebirdAdapter(DatabaseAdapter):
138
138
  )
139
139
 
140
140
  def fetch(self, sql: str, params: list = None,
141
- limit: int = 20, offset: int = 0) -> DatabaseResult:
141
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
142
142
  sql = self._translate_sql(sql)
143
143
  cursor = self._conn.cursor()
144
144
 
@@ -102,7 +102,7 @@ class MSSQLAdapter(DatabaseAdapter):
102
102
  )
103
103
 
104
104
  def fetch(self, sql: str, params: list = None,
105
- limit: int = 20, offset: int = 0) -> DatabaseResult:
105
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
106
106
  sql = self._translate_sql(sql)
107
107
  cursor = self._conn.cursor(as_dict=True)
108
108
 
@@ -91,7 +91,7 @@ class MySQLAdapter(DatabaseAdapter):
91
91
  )
92
92
 
93
93
  def fetch(self, sql: str, params: list = None,
94
- limit: int = 20, offset: int = 0) -> DatabaseResult:
94
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
95
95
  sql = self._translate_sql(sql)
96
96
  cursor = self._conn.cursor(dictionary=True)
97
97
 
@@ -74,7 +74,7 @@ class ODBCAdapter(DatabaseAdapter):
74
74
  )
75
75
 
76
76
  def fetch(self, sql: str, params: list = None,
77
- limit: int = 20, offset: int = 0) -> DatabaseResult:
77
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
78
78
  # Count total
79
79
  count_sql = f"SELECT COUNT(*) FROM ({sql}) AS _t"
80
80
  cursor = self._conn.cursor()
@@ -100,7 +100,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
100
100
  )
101
101
 
102
102
  def fetch(self, sql: str, params: list = None,
103
- limit: int = 20, offset: int = 0) -> DatabaseResult:
103
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
104
104
  import psycopg2.extras
105
105
 
106
106
  sql = self._translate_sql(sql)
@@ -93,7 +93,7 @@ class SQLiteAdapter(DatabaseAdapter):
93
93
  )
94
94
 
95
95
  def fetch(self, sql: str, params: list = None,
96
- limit: int = 20, offset: int = 0) -> DatabaseResult:
96
+ limit: int = 100, offset: int = 0) -> DatabaseResult:
97
97
  # Count total rows (without LIMIT/OFFSET)
98
98
  count_sql = f"SELECT COUNT(*) as cnt FROM ({sql})"
99
99
  try:
@@ -118,6 +118,7 @@ _SET_RE = re.compile(r"set\s+(\w+)\s*=\s*(.+)")
118
118
  _INCLUDE_RE = re.compile(r'include\s+["\'](.+?)["\'](?:\s+with\s+(.+))?')
119
119
  _MACRO_RE = re.compile(r"macro\s+(\w+)\s*\(([^)]*)\)")
120
120
  _FROM_IMPORT_RE = re.compile(r'from\s+["\'](.+?)["\']\s+import\s+(.+)')
121
+ _IMPORT_AS_RE = re.compile(r'import\s+["\'](.+?)["\']\s+as\s+(\w+)')
121
122
  _CACHE_RE = re.compile(r'cache\s+["\'](.+?)["\']\s*(\d+)?')
122
123
  _AUTOESCAPE_RE = re.compile(r"autoescape\s+(false|true)")
123
124
  _SPACELESS_RE = re.compile(r">\s+<")
@@ -1328,6 +1329,10 @@ class Frond:
1328
1329
  self._handle_from_import(content, context)
1329
1330
  i += 1
1330
1331
 
1332
+ elif tag == "import":
1333
+ self._handle_import_as(content, context)
1334
+ i += 1
1335
+
1331
1336
  elif tag == "cache":
1332
1337
  result, skip = self._handle_cache(tokens, i, context)
1333
1338
  output.append(result)
@@ -1803,6 +1808,70 @@ class Frond:
1803
1808
  continue
1804
1809
  i += 1
1805
1810
 
1811
+ def _handle_import_as(self, content: str, context: dict):
1812
+ """Handle {% import "file" as alias %}.
1813
+
1814
+ Loads ALL macros from the file and registers them as an object
1815
+ with methods, so {{ alias.macro_name(args) }} works.
1816
+ """
1817
+ m = _IMPORT_AS_RE.match(content)
1818
+ if not m:
1819
+ return
1820
+
1821
+ filename = m.group(1)
1822
+ alias = m.group(2)
1823
+
1824
+ # Load and tokenize the macro file
1825
+ source = self._load(filename)
1826
+ tokens = _tokenize(source)
1827
+
1828
+ # Collect all macro definitions
1829
+ macros = {}
1830
+ i = 0
1831
+ while i < len(tokens):
1832
+ ttype, raw = tokens[i]
1833
+ if ttype == BLOCK:
1834
+ tag_content, _, _ = _strip_tag(raw)
1835
+ tag = tag_content.split()[0] if tag_content.split() else ""
1836
+ if tag == "macro":
1837
+ macro_m = _MACRO_RE.match(tag_content)
1838
+ if macro_m:
1839
+ macro_name = macro_m.group(1)
1840
+ parsed_params = self._parse_macro_params(macro_m.group(2))
1841
+
1842
+ body_tokens = []
1843
+ i += 1
1844
+ while i < len(tokens):
1845
+ if tokens[i][0] == BLOCK and "endmacro" in tokens[i][1]:
1846
+ i += 1
1847
+ break
1848
+ body_tokens.append(tokens[i])
1849
+ i += 1
1850
+
1851
+ engine = self
1852
+ captured_body = list(body_tokens)
1853
+ captured_params = list(parsed_params)
1854
+ captured_context = dict(context)
1855
+
1856
+ def make_fn(_params, _body, _ctx):
1857
+ def fn(*args):
1858
+ macro_ctx = dict(_ctx)
1859
+ for pi, (pname, pdefault) in enumerate(_params):
1860
+ if pi < len(args):
1861
+ macro_ctx[pname] = args[pi]
1862
+ else:
1863
+ macro_ctx[pname] = pdefault
1864
+ return SafeString(engine._render_tokens(list(_body), macro_ctx))
1865
+ return fn
1866
+
1867
+ macros[macro_name] = make_fn(captured_params, captured_body, captured_context)
1868
+ continue
1869
+ i += 1
1870
+
1871
+ # Create a namespace object so alias.macro_name() works
1872
+ namespace = type("MacroNamespace", (), macros)()
1873
+ context[alias] = namespace
1874
+
1806
1875
  def _handle_cache(self, tokens: list, start: int, context: dict) -> tuple[str, int]:
1807
1876
  """Handle {% cache "key" ttl %}...{% endcache %}.
1808
1877
 
File without changes