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.
- {tina4_python-3.10.48 → tina4_python-3.10.50}/PKG-INFO +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/__init__.py +23 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/cli/__init__.py +18 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/request.py +15 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/router.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/server.py +4 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/adapter.py +23 -11
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/connection.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/firebird.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/mssql.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/mysql.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/odbc.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/postgres.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/sqlite.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/engine.py +69 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/.gitignore +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/README.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/pyproject.toml +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -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.
|
|
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("
|
|
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
|
-
|
|
217
|
-
|
|
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":
|
|
233
|
+
"type": col_type,
|
|
221
234
|
"size": None,
|
|
222
235
|
"decimals": None,
|
|
223
236
|
"nullable": True,
|
|
224
|
-
"primary_key":
|
|
225
|
-
}
|
|
226
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.50}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|