tina4-python 3.10.50__tar.gz → 3.10.55__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.50 → tina4_python-3.10.55}/PKG-INFO +1 -1
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/__init__.py +2 -2
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/cli/__init__.py +5 -2
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/router.py +55 -6
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/server.py +48 -9
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/crud/__init__.py +27 -8
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/adapter.py +11 -8
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/connection.py +8 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/firebird.py +1 -1
- tina4_python-3.10.55/tina4_python/database/mongodb.py +751 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/dev_admin/__init__.py +4 -2
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/orm/model.py +11 -1
- {tina4_python-3.10.50 → tina4_python-3.10.55}/.gitignore +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/README.md +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/pyproject.toml +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.50 → tina4_python-3.10.55}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -8,13 +8,13 @@ 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.55"
|
|
12
12
|
|
|
13
13
|
# ── Route decorators ──
|
|
14
14
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
15
15
|
get, post, put, patch, delete, any_method,
|
|
16
16
|
noauth, secured, cached, middleware, template,
|
|
17
|
-
Router,
|
|
17
|
+
Router, RouteGroup,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
# ── HTTP Constants ──
|
|
@@ -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
|
|
@@ -327,12 +327,15 @@ def _serve(args):
|
|
|
327
327
|
if os.environ.get("TINA4_OPEN_BROWSER", "").lower() in ("false", "0", "no"):
|
|
328
328
|
no_browser = True
|
|
329
329
|
|
|
330
|
+
# --no-reload flag
|
|
331
|
+
no_reload = "no-reload" in flags
|
|
332
|
+
|
|
330
333
|
# Kill existing process on port
|
|
331
334
|
port = cli_port or int(os.environ.get("PORT", os.environ.get("TINA4_PORT", "7146")))
|
|
332
335
|
_kill_process_on_port(port)
|
|
333
336
|
|
|
334
337
|
from tina4_python.core import run
|
|
335
|
-
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)
|
|
336
339
|
|
|
337
340
|
|
|
338
341
|
# ── Migrate ───────────────────────────────────────────────────────────
|
|
@@ -57,6 +57,51 @@ class RouteRef:
|
|
|
57
57
|
return self
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
class RouteGroup:
|
|
61
|
+
"""A group of routes sharing a common prefix and middleware.
|
|
62
|
+
|
|
63
|
+
Passed to the callback in Router.group(). Supports nesting.
|
|
64
|
+
|
|
65
|
+
Usage::
|
|
66
|
+
|
|
67
|
+
Router.group("/api", lambda group: [
|
|
68
|
+
group.get("/users", list_handler),
|
|
69
|
+
group.post("/users", create_handler),
|
|
70
|
+
group.group("/admin", lambda admin: [
|
|
71
|
+
admin.get("/stats", stats_handler),
|
|
72
|
+
], middleware=[admin_check]),
|
|
73
|
+
], middleware=[auth_check])
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, router_cls, prefix: str, middleware: list = None):
|
|
77
|
+
self._router = router_cls
|
|
78
|
+
self._prefix = prefix
|
|
79
|
+
self._middleware = middleware or []
|
|
80
|
+
|
|
81
|
+
def get(self, path: str, handler, **options) -> RouteRef:
|
|
82
|
+
return self._router.add("GET", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
83
|
+
|
|
84
|
+
def post(self, path: str, handler, **options) -> RouteRef:
|
|
85
|
+
return self._router.add("POST", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
86
|
+
|
|
87
|
+
def put(self, path: str, handler, **options) -> RouteRef:
|
|
88
|
+
return self._router.add("PUT", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
89
|
+
|
|
90
|
+
def patch(self, path: str, handler, **options) -> RouteRef:
|
|
91
|
+
return self._router.add("PATCH", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
92
|
+
|
|
93
|
+
def delete(self, path: str, handler, **options) -> RouteRef:
|
|
94
|
+
return self._router.add("DELETE", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
95
|
+
|
|
96
|
+
def any(self, path: str, handler, **options) -> RouteRef:
|
|
97
|
+
return self._router.add("ANY", self._prefix + path, handler, middleware=self._middleware, **options)
|
|
98
|
+
|
|
99
|
+
def group(self, prefix: str, callback, middleware=None):
|
|
100
|
+
merged = list(self._middleware) + (middleware or [])
|
|
101
|
+
nested = RouteGroup(self._router, self._prefix + prefix.rstrip("/"), merged)
|
|
102
|
+
callback(nested)
|
|
103
|
+
|
|
104
|
+
|
|
60
105
|
class Router:
|
|
61
106
|
"""Route registry and matcher."""
|
|
62
107
|
|
|
@@ -68,14 +113,17 @@ class Router:
|
|
|
68
113
|
def group(cls, prefix: str, callback, middleware=None):
|
|
69
114
|
"""Register routes with a shared prefix and optional middleware.
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
The callback receives a RouteGroup object with get/post/put/patch/
|
|
117
|
+
delete/any/group methods for registering routes under the prefix.
|
|
73
118
|
|
|
74
119
|
Usage::
|
|
75
120
|
|
|
76
|
-
Router.group("/api", lambda: [
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
Router.group("/api", lambda group: [
|
|
122
|
+
group.get("/users", list_handler),
|
|
123
|
+
group.post("/users", create_handler),
|
|
124
|
+
group.group("/admin", lambda admin: [
|
|
125
|
+
admin.get("/stats", stats_handler),
|
|
126
|
+
], middleware=[admin_check]),
|
|
79
127
|
], middleware=[auth_check])
|
|
80
128
|
"""
|
|
81
129
|
prev_prefix = cls._group_prefix
|
|
@@ -85,7 +133,8 @@ class Router:
|
|
|
85
133
|
cls._group_middleware = prev_middleware + (middleware or [])
|
|
86
134
|
|
|
87
135
|
try:
|
|
88
|
-
|
|
136
|
+
group = RouteGroup(cls, cls._group_prefix, list(cls._group_middleware))
|
|
137
|
+
callback(group)
|
|
89
138
|
finally:
|
|
90
139
|
cls._group_prefix = prev_prefix
|
|
91
140
|
cls._group_middleware = prev_middleware
|
|
@@ -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
|
|
|
@@ -1222,7 +1226,7 @@ def resolve_config(cli_host: str | None = None, cli_port: int | None = None) ->
|
|
|
1222
1226
|
return host, port
|
|
1223
1227
|
|
|
1224
1228
|
|
|
1225
|
-
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):
|
|
1226
1230
|
"""Print the Tina4 Slant ASCII banner to stdout (not through the logger)."""
|
|
1227
1231
|
from tina4_python.dotenv import is_truthy
|
|
1228
1232
|
|
|
@@ -1234,6 +1238,8 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
|
1234
1238
|
color = "\033[34m" if sys.stdout.isatty() else ""
|
|
1235
1239
|
reset = "\033[0m" if sys.stdout.isatty() else ""
|
|
1236
1240
|
|
|
1241
|
+
ai_port_line = f"\n AI Port: http://{display}:{ai_port} (no-reload)" if ai_port else ""
|
|
1242
|
+
|
|
1237
1243
|
banner = f"""{color}
|
|
1238
1244
|
______ _ __ __
|
|
1239
1245
|
/_ __/(_)___ ____ _/ // /
|
|
@@ -1246,12 +1252,12 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
|
1246
1252
|
Server: http://{display}:{port} ({server_name})
|
|
1247
1253
|
Swagger: http://localhost:{port}/swagger
|
|
1248
1254
|
Dashboard: http://localhost:{port}/__dev
|
|
1249
|
-
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}
|
|
1250
1256
|
"""
|
|
1251
1257
|
print(banner)
|
|
1252
1258
|
|
|
1253
1259
|
|
|
1254
|
-
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):
|
|
1255
1261
|
"""Start the Tina4 dev server.
|
|
1256
1262
|
|
|
1257
1263
|
Discovers routes from src/, starts ASGI server, handles shutdown.
|
|
@@ -1260,11 +1266,15 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1260
1266
|
host: Bind address. Falls back to HOST env var, then 0.0.0.0.
|
|
1261
1267
|
port: Bind port. Falls back to PORT env var, then 7146.
|
|
1262
1268
|
no_browser: If True, do not open browser on startup.
|
|
1269
|
+
no_reload: If True, disable the file watcher / live-reload.
|
|
1263
1270
|
"""
|
|
1264
1271
|
import time
|
|
1265
1272
|
global _start_time
|
|
1266
1273
|
_start_time = time.time()
|
|
1267
1274
|
|
|
1275
|
+
if no_reload:
|
|
1276
|
+
os.environ["TINA4_NO_RELOAD"] = "true"
|
|
1277
|
+
|
|
1268
1278
|
# Ensure CWD is on sys.path so auto-discovered modules can be imported
|
|
1269
1279
|
cwd = os.getcwd()
|
|
1270
1280
|
if cwd not in sys.path:
|
|
@@ -1302,11 +1312,13 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1302
1312
|
|
|
1303
1313
|
# Start DevReload file watcher in debug mode
|
|
1304
1314
|
if is_debug:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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}")
|
|
1310
1322
|
|
|
1311
1323
|
prod = None
|
|
1312
1324
|
if not is_debug:
|
|
@@ -1314,11 +1326,17 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1314
1326
|
|
|
1315
1327
|
server_name = prod[0] if prod else "asyncio"
|
|
1316
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
|
+
|
|
1317
1333
|
# Banner — printed directly to stdout, not through the logger
|
|
1318
|
-
_print_banner(host, port, server_name)
|
|
1334
|
+
_print_banner(host, port, server_name, ai_port=_ai_port)
|
|
1319
1335
|
|
|
1320
1336
|
display = "localhost" if host in ("0.0.0.0", "::") else host
|
|
1321
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)")
|
|
1322
1340
|
|
|
1323
1341
|
# Open browser after a short delay (unless --no-browser)
|
|
1324
1342
|
_skip_browser = no_browser or os.environ.get("TINA4_NO_BROWSER", "").lower() in ("true", "1", "yes")
|
|
@@ -1387,6 +1405,11 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1387
1405
|
# Check for WebSocket upgrade before reading body
|
|
1388
1406
|
_header_dict = {k.decode(): v.decode() for k, v in headers}
|
|
1389
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
|
|
1390
1413
|
await _handle_dev_websocket(reader, writer, _header_dict, path)
|
|
1391
1414
|
return
|
|
1392
1415
|
|
|
@@ -1441,6 +1464,19 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1441
1464
|
|
|
1442
1465
|
server = await start_server(_handle_connection, host, port)
|
|
1443
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
|
+
|
|
1444
1480
|
loop = asyncio.get_running_loop()
|
|
1445
1481
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
1446
1482
|
try:
|
|
@@ -1449,6 +1485,9 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1449
1485
|
pass # Windows
|
|
1450
1486
|
|
|
1451
1487
|
await shutdown.wait()
|
|
1488
|
+
if ai_server:
|
|
1489
|
+
ai_server.close()
|
|
1490
|
+
await ai_server.wait_closed()
|
|
1452
1491
|
server.close()
|
|
1453
1492
|
await server.wait_closed()
|
|
1454
1493
|
Log.info("Server stopped.")
|
|
@@ -15,7 +15,7 @@ Discovers ORM models and registers CRUD routes automatically.
|
|
|
15
15
|
|
|
16
16
|
Generated endpoints per model:
|
|
17
17
|
|
|
18
|
-
GET /api/{table_name} — list with pagination (limit,
|
|
18
|
+
GET /api/{table_name} — list with pagination (limit, offset; also accepts page, per_page)
|
|
19
19
|
GET /api/{table_name}/{id} — get single record by primary key
|
|
20
20
|
POST /api/{table_name} — create new record
|
|
21
21
|
PUT /api/{table_name}/{id} — update record by primary key
|
|
@@ -94,18 +94,37 @@ class AutoCrud:
|
|
|
94
94
|
# ── GET /api/{table} — list with pagination ──────────────
|
|
95
95
|
async def list_handler(request, response, _cls=model_class):
|
|
96
96
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
# Primary names: limit / offset
|
|
98
|
+
# Compat names: per_page / page (PHP/Ruby/Node style)
|
|
99
|
+
limit = int(request.params.get("limit", request.params.get("per_page", 10)))
|
|
100
|
+
offset = int(request.params.get("offset", 0))
|
|
101
|
+
# page/per_page compat: if page is provided, derive offset from it
|
|
102
|
+
if "page" in request.params and "offset" not in request.params:
|
|
103
|
+
page = int(request.params.get("page", 1))
|
|
104
|
+
per_page = int(request.params.get("per_page", limit))
|
|
105
|
+
offset = (page - 1) * per_page
|
|
106
|
+
limit = per_page
|
|
107
|
+
else:
|
|
108
|
+
page = (offset // limit) + 1 if limit else 1
|
|
99
109
|
except (ValueError, TypeError):
|
|
100
110
|
limit = 10
|
|
101
|
-
|
|
111
|
+
offset = 0
|
|
112
|
+
page = 1
|
|
102
113
|
|
|
103
|
-
records, total = _cls.all(limit=limit, skip=
|
|
114
|
+
records, total = _cls.all(limit=limit, skip=offset)
|
|
115
|
+
total_pages = max(1, -(-total // limit)) if limit else 1
|
|
116
|
+
data = [r.to_dict() for r in records]
|
|
104
117
|
return response({
|
|
105
|
-
"
|
|
106
|
-
"
|
|
118
|
+
"records": data, # standard name
|
|
119
|
+
"data": data, # backwards compat
|
|
120
|
+
"count": total, # standard name
|
|
121
|
+
"total": total, # backwards compat
|
|
107
122
|
"limit": limit,
|
|
108
|
-
"
|
|
123
|
+
"offset": offset,
|
|
124
|
+
"page": page,
|
|
125
|
+
"per_page": limit, # backwards compat
|
|
126
|
+
"totalPages": total_pages, # camelCase standard
|
|
127
|
+
"total_pages": total_pages, # backwards compat
|
|
109
128
|
})
|
|
110
129
|
|
|
111
130
|
list_handler.__name__ = f"autocrud_list_{table}"
|
|
@@ -33,16 +33,19 @@ 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
|
-
|
|
37
|
-
|
|
36
|
+
offset = (page - 1) * per_page
|
|
37
|
+
data = self.records[offset:offset + per_page]
|
|
38
38
|
return {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"records": data, # standard name
|
|
40
|
+
"data": data, # backwards compat (PHP/Ruby/Node)
|
|
41
|
+
"count": self.count, # standard name
|
|
42
|
+
"total": self.count, # backwards compat
|
|
43
|
+
"limit": per_page, # standard name
|
|
44
|
+
"offset": offset, # standard name
|
|
41
45
|
"page": page,
|
|
42
|
-
"per_page": per_page,
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"has_prev": page > 1,
|
|
46
|
+
"per_page": per_page, # backwards compat
|
|
47
|
+
"totalPages": total_pages, # camelCase standard
|
|
48
|
+
"total_pages": total_pages, # backwards compat
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
def column_info(self) -> list[dict]:
|
|
@@ -123,6 +123,14 @@ register_driver("sqlserver", MSSQLAdapter)
|
|
|
123
123
|
from tina4_python.database.firebird import FirebirdAdapter
|
|
124
124
|
register_driver("firebird", FirebirdAdapter)
|
|
125
125
|
|
|
126
|
+
# Register MongoDB (pymongo — optional)
|
|
127
|
+
try:
|
|
128
|
+
from tina4_python.database.mongodb import MongoDBAdapter
|
|
129
|
+
register_driver("mongodb", MongoDBAdapter)
|
|
130
|
+
register_driver("pymongo", MongoDBAdapter)
|
|
131
|
+
except ImportError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
126
134
|
|
|
127
135
|
class Database:
|
|
128
136
|
"""Database connection manager.
|
|
@@ -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")
|