tina4-python 3.10.48__tar.gz → 3.10.54__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.54}/PKG-INFO +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/__init__.py +23 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/cli/__init__.py +23 -2
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/request.py +15 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/router.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/server.py +52 -9
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/adapter.py +23 -11
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/connection.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/firebird.py +2 -2
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/mssql.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/mysql.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/odbc.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/postgres.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/sqlite.py +1 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/dev_admin/__init__.py +4 -2
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/frond/engine.py +69 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/orm/model.py +11 -1
- {tina4_python-3.10.48 → tina4_python-3.10.54}/.gitignore +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/README.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/pyproject.toml +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.48 → tina4_python-3.10.54}/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.54"
|
|
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
|
|
@@ -156,7 +156,7 @@ Usage: tina4python <command> [options]
|
|
|
156
156
|
|
|
157
157
|
Commands:
|
|
158
158
|
init [dir] Scaffold a new project
|
|
159
|
-
serve [--port P] [--no-browser] Start dev server (default: 0.0.0.0:7146)
|
|
159
|
+
serve [--port P] [--no-browser] [--no-reload] Start dev server (default: 0.0.0.0:7146)
|
|
160
160
|
migrate Run pending database migrations
|
|
161
161
|
migrate:create <desc> Create a new migration file
|
|
162
162
|
migrate:rollback Rollback last migration batch
|
|
@@ -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():
|
|
@@ -309,12 +327,15 @@ def _serve(args):
|
|
|
309
327
|
if os.environ.get("TINA4_OPEN_BROWSER", "").lower() in ("false", "0", "no"):
|
|
310
328
|
no_browser = True
|
|
311
329
|
|
|
330
|
+
# --no-reload flag
|
|
331
|
+
no_reload = "no-reload" in flags
|
|
332
|
+
|
|
312
333
|
# Kill existing process on port
|
|
313
334
|
port = cli_port or int(os.environ.get("PORT", os.environ.get("TINA4_PORT", "7146")))
|
|
314
335
|
_kill_process_on_port(port)
|
|
315
336
|
|
|
316
337
|
from tina4_python.core import run
|
|
317
|
-
run(host=cli_host, port=cli_port, no_browser=no_browser)
|
|
338
|
+
run(host=cli_host, port=cli_port, no_browser=no_browser, no_reload=no_reload)
|
|
318
339
|
|
|
319
340
|
|
|
320
341
|
# ── Migrate ───────────────────────────────────────────────────────────
|
|
@@ -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("}"):
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import sys
|
|
10
10
|
import signal
|
|
11
11
|
import asyncio
|
|
12
|
+
import contextvars
|
|
12
13
|
import importlib
|
|
13
14
|
import uuid
|
|
14
15
|
from pathlib import Path
|
|
@@ -25,6 +26,9 @@ _cors = CorsMiddleware()
|
|
|
25
26
|
_rate_limiter = RateLimiter()
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
# ContextVar to signal that the current request is being served on the AI dev port
|
|
30
|
+
_ai_port_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar("_ai_port_ctx", default=False)
|
|
31
|
+
|
|
28
32
|
# Track startup time
|
|
29
33
|
_start_time: float = 0
|
|
30
34
|
|
|
@@ -640,6 +644,10 @@ def _init_session(request: Request) -> None:
|
|
|
640
644
|
sess = Session()
|
|
641
645
|
sess.start(sid_match)
|
|
642
646
|
request.session = sess
|
|
647
|
+
# Probabilistic garbage collection (1% of requests)
|
|
648
|
+
import random
|
|
649
|
+
if random.randint(1, 100) == 1:
|
|
650
|
+
sess.gc()
|
|
643
651
|
except Exception:
|
|
644
652
|
pass # Session module not available — session stays None
|
|
645
653
|
|
|
@@ -1218,7 +1226,7 @@ def resolve_config(cli_host: str | None = None, cli_port: int | None = None) ->
|
|
|
1218
1226
|
return host, port
|
|
1219
1227
|
|
|
1220
1228
|
|
|
1221
|
-
def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
1229
|
+
def _print_banner(host: str, port: int, server_name: str = "asyncio", ai_port: int | None = None):
|
|
1222
1230
|
"""Print the Tina4 Slant ASCII banner to stdout (not through the logger)."""
|
|
1223
1231
|
from tina4_python.dotenv import is_truthy
|
|
1224
1232
|
|
|
@@ -1230,6 +1238,8 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
|
1230
1238
|
color = "\033[34m" if sys.stdout.isatty() else ""
|
|
1231
1239
|
reset = "\033[0m" if sys.stdout.isatty() else ""
|
|
1232
1240
|
|
|
1241
|
+
ai_port_line = f"\n AI Port: http://{display}:{ai_port} (no-reload)" if ai_port else ""
|
|
1242
|
+
|
|
1233
1243
|
banner = f"""{color}
|
|
1234
1244
|
______ _ __ __
|
|
1235
1245
|
/_ __/(_)___ ____ _/ // /
|
|
@@ -1242,12 +1252,12 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
|
1242
1252
|
Server: http://{display}:{port} ({server_name})
|
|
1243
1253
|
Swagger: http://localhost:{port}/swagger
|
|
1244
1254
|
Dashboard: http://localhost:{port}/__dev
|
|
1245
|
-
Debug: {"ON" if is_debug else "OFF"} (Log level: {log_level})
|
|
1255
|
+
Debug: {"ON" if is_debug else "OFF"} (Log level: {log_level}){ai_port_line}
|
|
1246
1256
|
"""
|
|
1247
1257
|
print(banner)
|
|
1248
1258
|
|
|
1249
1259
|
|
|
1250
|
-
def run(host: str | None = None, port: int | None = None, no_browser: bool = False):
|
|
1260
|
+
def run(host: str | None = None, port: int | None = None, no_browser: bool = False, no_reload: bool = False):
|
|
1251
1261
|
"""Start the Tina4 dev server.
|
|
1252
1262
|
|
|
1253
1263
|
Discovers routes from src/, starts ASGI server, handles shutdown.
|
|
@@ -1256,11 +1266,15 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1256
1266
|
host: Bind address. Falls back to HOST env var, then 0.0.0.0.
|
|
1257
1267
|
port: Bind port. Falls back to PORT env var, then 7146.
|
|
1258
1268
|
no_browser: If True, do not open browser on startup.
|
|
1269
|
+
no_reload: If True, disable the file watcher / live-reload.
|
|
1259
1270
|
"""
|
|
1260
1271
|
import time
|
|
1261
1272
|
global _start_time
|
|
1262
1273
|
_start_time = time.time()
|
|
1263
1274
|
|
|
1275
|
+
if no_reload:
|
|
1276
|
+
os.environ["TINA4_NO_RELOAD"] = "true"
|
|
1277
|
+
|
|
1264
1278
|
# Ensure CWD is on sys.path so auto-discovered modules can be imported
|
|
1265
1279
|
cwd = os.getcwd()
|
|
1266
1280
|
if cwd not in sys.path:
|
|
@@ -1298,11 +1312,13 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1298
1312
|
|
|
1299
1313
|
# Start DevReload file watcher in debug mode
|
|
1300
1314
|
if is_debug:
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1315
|
+
no_reload = os.environ.get("TINA4_NO_RELOAD", "").lower() in ("true", "1", "yes")
|
|
1316
|
+
if not no_reload:
|
|
1317
|
+
try:
|
|
1318
|
+
from tina4_python.dev_reload import start as _start_dev_reload
|
|
1319
|
+
_start_dev_reload(["src", "public"])
|
|
1320
|
+
except Exception as e:
|
|
1321
|
+
Log.error(f"DevReload: failed to start: {e}")
|
|
1306
1322
|
|
|
1307
1323
|
prod = None
|
|
1308
1324
|
if not is_debug:
|
|
@@ -1310,11 +1326,17 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1310
1326
|
|
|
1311
1327
|
server_name = prod[0] if prod else "asyncio"
|
|
1312
1328
|
|
|
1329
|
+
# Determine AI dev port (port+1) when debug is on and not suppressed
|
|
1330
|
+
_no_ai_port = os.environ.get("TINA4_NO_AI_PORT", "").lower() in ("true", "1", "yes")
|
|
1331
|
+
_ai_port = (port + 1) if (is_debug and not _no_ai_port) else None
|
|
1332
|
+
|
|
1313
1333
|
# Banner — printed directly to stdout, not through the logger
|
|
1314
|
-
_print_banner(host, port, server_name)
|
|
1334
|
+
_print_banner(host, port, server_name, ai_port=_ai_port)
|
|
1315
1335
|
|
|
1316
1336
|
display = "localhost" if host in ("0.0.0.0", "::") else host
|
|
1317
1337
|
Log.info(f"Server started http://{display}:{port} ({server_name})")
|
|
1338
|
+
if _ai_port:
|
|
1339
|
+
Log.info(f"AI dev port: http://{display}:{_ai_port} (no-reload)")
|
|
1318
1340
|
|
|
1319
1341
|
# Open browser after a short delay (unless --no-browser)
|
|
1320
1342
|
_skip_browser = no_browser or os.environ.get("TINA4_NO_BROWSER", "").lower() in ("true", "1", "yes")
|
|
@@ -1383,6 +1405,11 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1383
1405
|
# Check for WebSocket upgrade before reading body
|
|
1384
1406
|
_header_dict = {k.decode(): v.decode() for k, v in headers}
|
|
1385
1407
|
if _header_dict.get("upgrade", "").lower() == "websocket":
|
|
1408
|
+
if hasattr(writer, "_tina4_ai_port") and path == "/__dev_reload":
|
|
1409
|
+
writer.write(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
|
|
1410
|
+
await writer.drain()
|
|
1411
|
+
writer.close()
|
|
1412
|
+
return
|
|
1386
1413
|
await _handle_dev_websocket(reader, writer, _header_dict, path)
|
|
1387
1414
|
return
|
|
1388
1415
|
|
|
@@ -1437,6 +1464,19 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1437
1464
|
|
|
1438
1465
|
server = await start_server(_handle_connection, host, port)
|
|
1439
1466
|
|
|
1467
|
+
# AI dev port (port + 1) — no-reload, no live-reload WebSocket
|
|
1468
|
+
ai_server = None
|
|
1469
|
+
if _ai_port:
|
|
1470
|
+
try:
|
|
1471
|
+
async def _handle_ai_connection(reader, writer):
|
|
1472
|
+
_ai_port_ctx.set(True)
|
|
1473
|
+
writer._tina4_ai_port = True
|
|
1474
|
+
await _handle_connection(reader, writer)
|
|
1475
|
+
|
|
1476
|
+
ai_server = await start_server(_handle_ai_connection, host, _ai_port)
|
|
1477
|
+
except OSError:
|
|
1478
|
+
Log.warning(f"AI port {_ai_port} in use — skipping")
|
|
1479
|
+
|
|
1440
1480
|
loop = asyncio.get_running_loop()
|
|
1441
1481
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
1442
1482
|
try:
|
|
@@ -1445,6 +1485,9 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1445
1485
|
pass # Windows
|
|
1446
1486
|
|
|
1447
1487
|
await shutdown.wait()
|
|
1488
|
+
if ai_server:
|
|
1489
|
+
ai_server.close()
|
|
1490
|
+
await ai_server.wait_closed()
|
|
1448
1491
|
server.close()
|
|
1449
1492
|
await server.wait_closed()
|
|
1450
1493
|
Log.info("Server stopped.")
|
|
@@ -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)
|
|
@@ -49,7 +49,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
49
49
|
host = parsed.hostname or "localhost"
|
|
50
50
|
port = parsed.port or 3050
|
|
51
51
|
# Firebird database path — decode URL-encoded characters
|
|
52
|
-
db_path = unquote(parsed.path.
|
|
52
|
+
db_path = unquote(parsed.path[1:]) if parsed.path.startswith("/") else unquote(parsed.path)
|
|
53
53
|
user = parsed.username or username or "SYSDBA"
|
|
54
54
|
password = parsed.password or password or "masterkey"
|
|
55
55
|
charset = kwargs.pop("charset", "UTF8")
|
|
@@ -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:
|
|
@@ -2447,8 +2447,10 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2447
2447
|
and a close button.
|
|
2448
2448
|
"""
|
|
2449
2449
|
import sys
|
|
2450
|
+
from tina4_python.core.server import _ai_port_ctx
|
|
2450
2451
|
python_version = sys.version.split()[0]
|
|
2451
2452
|
poll_interval_ms = int(os.environ.get("TINA4_DEV_POLL_INTERVAL", "3000"))
|
|
2453
|
+
no_reload = os.environ.get("TINA4_NO_RELOAD", "").lower() in ("true", "1", "yes") or _ai_port_ctx.get()
|
|
2452
2454
|
|
|
2453
2455
|
return f"""<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
|
|
2454
2456
|
<span id="tina4-ver-btn" style="color:#3572A5;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v{__version__}</span>
|
|
@@ -2472,7 +2474,7 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2472
2474
|
<span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">✕</span>
|
|
2473
2475
|
</div>
|
|
2474
2476
|
<script>
|
|
2475
|
-
(function(){{
|
|
2477
|
+
{'(function(){})();' if no_reload else f"""(function(){{
|
|
2476
2478
|
var _t4_mtime=0,_t4_css_exts=['.css','.scss'],_t4_debounce=null;
|
|
2477
2479
|
var _t4_interval=parseInt('{poll_interval_ms}')||3000;
|
|
2478
2480
|
function _t4_apply(d){{
|
|
@@ -2499,7 +2501,7 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2499
2501
|
}}).catch(function(){{}});
|
|
2500
2502
|
}}
|
|
2501
2503
|
setInterval(_t4_poll,_t4_interval);
|
|
2502
|
-
}})();
|
|
2504
|
+
}})();"""}
|
|
2503
2505
|
function tina4VersionModal(){{
|
|
2504
2506
|
var m=document.getElementById('tina4-ver-modal');
|
|
2505
2507
|
if(m.style.display==='block'){{m.style.display='none';return;}}
|
|
@@ -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
|
|
|
@@ -204,8 +204,18 @@ class ORM(metaclass=ORMMeta):
|
|
|
204
204
|
return cls._db # Direct Database instance
|
|
205
205
|
|
|
206
206
|
if _database is None:
|
|
207
|
+
# Try auto-discovery from DATABASE_URL
|
|
208
|
+
import os
|
|
209
|
+
url = os.environ.get("DATABASE_URL")
|
|
210
|
+
if url:
|
|
211
|
+
from tina4_python.database import Database
|
|
212
|
+
username = os.environ.get("DATABASE_USERNAME", "")
|
|
213
|
+
password = os.environ.get("DATABASE_PASSWORD", "")
|
|
214
|
+
global _database
|
|
215
|
+
_database = Database(url, username, password)
|
|
216
|
+
return _database
|
|
207
217
|
raise RuntimeError(
|
|
208
|
-
"No database bound. Call orm_bind(db)
|
|
218
|
+
"No database bound. Call orm_bind(db) or set DATABASE_URL in .env"
|
|
209
219
|
)
|
|
210
220
|
return _database
|
|
211
221
|
|
|
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.54}/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.54}/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.54}/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.54}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/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.54}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/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.54}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.48 → tina4_python-3.10.54}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|