tina4-python 0.2.205__tar.gz → 0.2.206__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-0.2.205 → tina4_python-0.2.206}/PKG-INFO +2 -1
- {tina4_python-0.2.205 → tina4_python-0.2.206}/pyproject.toml +2 -1
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Response.py +4 -5
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Router.py +17 -4
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Template.py +66 -66
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/__init__.py +55 -34
- tina4_python-0.2.205/tina4_python/TwigEngine.py +0 -1255
- {tina4_python-0.2.205 → tina4_python-0.2.206}/.gitignore +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/README.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Api.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Auth.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/CRUD.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Constant.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Database.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/DatabaseResult.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/DatabaseTypes.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Debug.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/DevReload.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Env.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/FieldTypes.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/GraphQL.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Localization.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Messages.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/MiddleWare.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Migration.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/ORM.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Queue.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Request.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/SQLToMongo.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Seeder.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Session.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/ShellColors.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Swagger.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Testing.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/WSDL.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Webserver.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/Websocket.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/cli.py +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/messages.pot +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/js/reconnecting-websocket.js +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/js/tina4.js +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/js/tina4helper.js +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.205 → tina4_python-0.2.206}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.206
|
|
4
4
|
Summary: Tina4Python - This is not another framework for Python
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
Requires-Python: <4.0,>=3.12
|
|
7
7
|
Requires-Dist: bcrypt<5.0.0,>=4.2.1
|
|
8
8
|
Requires-Dist: hypercorn>=0.18.0
|
|
9
|
+
Requires-Dist: jinja2<4.0.0,>=3.1.5
|
|
9
10
|
Requires-Dist: libsass<0.24.0,>=0.23.0
|
|
10
11
|
Requires-Dist: litequeue<0.10,>=0.9
|
|
11
12
|
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tina4-python"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.206"
|
|
4
4
|
description = "Tina4Python - This is not another framework for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
|
|
@@ -8,6 +8,7 @@ authors = [
|
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.12,<4.0"
|
|
10
10
|
dependencies = [
|
|
11
|
+
"jinja2>=3.1.5,<4.0.0",
|
|
11
12
|
"libsass (>=0.23.0,<0.24.0)",
|
|
12
13
|
"python-dotenv (>=1.0.1,<2.0.0)",
|
|
13
14
|
"pyjwt>=2.10.1,<3.0.0",
|
|
@@ -104,12 +104,11 @@ class Response:
|
|
|
104
104
|
|
|
105
105
|
# Merge any headers added via add_header() before this Response was created
|
|
106
106
|
pending = _pending_headers.get()
|
|
107
|
+
merged_headers = {}
|
|
108
|
+
if pending:
|
|
109
|
+
merged_headers.update(pending)
|
|
107
110
|
if headers_in is not None:
|
|
108
|
-
merged_headers
|
|
109
|
-
elif pending:
|
|
110
|
-
merged_headers = dict(pending)
|
|
111
|
-
else:
|
|
112
|
-
merged_headers = {}
|
|
111
|
+
merged_headers.update(headers_in)
|
|
113
112
|
|
|
114
113
|
self.headers = merged_headers
|
|
115
114
|
self.content = content_in if content_in is not None else ""
|
|
@@ -501,10 +501,23 @@ class Router:
|
|
|
501
501
|
with open(cached_static, 'rb') as file:
|
|
502
502
|
return Response(file.read(), Constant.HTTP_OK, mime_type)
|
|
503
503
|
|
|
504
|
-
#
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
# Build candidate route list using prefix index (O(1) lookup vs O(n) scan)
|
|
505
|
+
url_segments = Router._normalize_request_url(url)
|
|
506
|
+
first_seg = url_segments[0] if url_segments else ""
|
|
507
|
+
|
|
508
|
+
# Collect indexed candidates
|
|
509
|
+
candidates = set()
|
|
510
|
+
if first_seg in Router._route_index:
|
|
511
|
+
candidates.update(Router._route_index[first_seg])
|
|
512
|
+
candidates.update(Router._wildcard_routes)
|
|
513
|
+
if not url_segments and "" in Router._route_index:
|
|
514
|
+
candidates.update(Router._route_index[""])
|
|
515
|
+
|
|
516
|
+
# If index is empty (e.g. tests reset tina4_routes directly), fall back to full scan
|
|
517
|
+
if not Router._route_index and not Router._wildcard_routes:
|
|
518
|
+
route_iter = tina4_python.tina4_routes.values()
|
|
519
|
+
else:
|
|
520
|
+
route_iter = (tina4_python.tina4_routes[cb] for cb in candidates if cb in tina4_python.tina4_routes)
|
|
508
521
|
|
|
509
522
|
buffer = io.StringIO()
|
|
510
523
|
for route in route_iter:
|
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
-
"""
|
|
7
|
+
"""Jinja2-based template engine for Tina4.
|
|
8
8
|
|
|
9
|
-
The ``Template`` class wraps
|
|
10
|
-
|
|
11
|
-
from ``src/templates/``.
|
|
9
|
+
The ``Template`` class wraps Jinja2 to provide Twig-compatible HTML
|
|
10
|
+
rendering with automatic template discovery from ``src/templates/``.
|
|
12
11
|
|
|
13
12
|
Features:
|
|
14
|
-
- Automatic
|
|
13
|
+
- Automatic ``FileSystemLoader`` setup scanning ``src/templates/``
|
|
15
14
|
- Built-in filters: ``json_decode``, ``base64_encode``, ``date``,
|
|
16
15
|
``slugify``, and more
|
|
17
16
|
- Built-in globals: ``url``, ``root``, ``session``, ``uniqid``, ``localize``
|
|
@@ -37,9 +36,9 @@ import base64
|
|
|
37
36
|
import tina4_python
|
|
38
37
|
from tina4_python import Constant
|
|
39
38
|
from tina4_python.Debug import Debug
|
|
40
|
-
from tina4_python.TwigEngine import TwigEngine, TemplateNotFound
|
|
41
39
|
from pathlib import Path
|
|
42
40
|
from datetime import datetime, date
|
|
41
|
+
from jinja2 import Environment, FileSystemLoader, Undefined, TemplateNotFound
|
|
43
42
|
from tina4_python.Session import Session
|
|
44
43
|
from random import random as RANDOM
|
|
45
44
|
from typing import Dict, Any
|
|
@@ -55,34 +54,36 @@ class Template:
|
|
|
55
54
|
|
|
56
55
|
@staticmethod
|
|
57
56
|
def add_filter(name, func):
|
|
58
|
-
"""Register a custom
|
|
57
|
+
"""Register a custom Jinja2 filter."""
|
|
59
58
|
Template._custom_filters[name] = func
|
|
60
59
|
if Template.twig is not None:
|
|
61
|
-
Template.twig.
|
|
60
|
+
Template.twig.filters[name] = func
|
|
62
61
|
|
|
63
62
|
@staticmethod
|
|
64
63
|
def add_global(name, value):
|
|
65
|
-
"""Register a custom
|
|
64
|
+
"""Register a custom Jinja2 global (function or value)."""
|
|
66
65
|
Template._custom_globals[name] = value
|
|
67
66
|
if Template.twig is not None:
|
|
68
|
-
Template.twig.
|
|
67
|
+
Template.twig.globals[name] = value
|
|
69
68
|
|
|
70
69
|
@staticmethod
|
|
71
70
|
def add_test(name, func):
|
|
72
|
-
"""Register a custom
|
|
71
|
+
"""Register a custom Jinja2 test for use with {% if x is testname %}."""
|
|
73
72
|
Template._custom_tests[name] = func
|
|
74
73
|
if Template.twig is not None:
|
|
75
|
-
Template.twig.
|
|
74
|
+
Template.twig.tests[name] = func
|
|
76
75
|
|
|
77
76
|
@staticmethod
|
|
78
77
|
def add_extension(extension):
|
|
79
|
-
"""Register a
|
|
78
|
+
"""Register a Jinja2 extension class."""
|
|
80
79
|
if extension not in Template._custom_extensions:
|
|
81
80
|
Template._custom_extensions.append(extension)
|
|
81
|
+
if Template.twig is not None:
|
|
82
|
+
Template.twig.add_extension(extension)
|
|
82
83
|
|
|
83
84
|
@staticmethod
|
|
84
85
|
def get_environment():
|
|
85
|
-
"""Return the underlying
|
|
86
|
+
"""Return the underlying Jinja2 Environment instance, initializing if needed."""
|
|
86
87
|
if Template.twig is None:
|
|
87
88
|
Template.init_twig(tina4_python.root_path + os.sep + "src" + os.sep + "templates")
|
|
88
89
|
return Template.twig
|
|
@@ -104,55 +105,52 @@ class Template:
|
|
|
104
105
|
return Template.twig
|
|
105
106
|
Debug.debug("Initializing Twig on " + path)
|
|
106
107
|
twig_path = Path(path)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if fw_templates.is_dir() and str(fw_templates) != str(twig_path):
|
|
111
|
-
search_paths.append(str(fw_templates))
|
|
112
|
-
Template.twig = TwigEngine(search_paths)
|
|
113
|
-
|
|
108
|
+
Template.twig = Environment(loader=FileSystemLoader(Path(twig_path)))
|
|
109
|
+
Template.twig.add_extension('jinja2.ext.debug')
|
|
110
|
+
Template.twig.add_extension('jinja2.ext.do')
|
|
114
111
|
# i18n translation function — available as _() global and | translate filter
|
|
115
112
|
from tina4_python.Localization import localize
|
|
116
113
|
_translate = localize()
|
|
117
|
-
Template.twig.
|
|
118
|
-
Template.twig.
|
|
119
|
-
Template.twig.
|
|
120
|
-
Template.twig.
|
|
121
|
-
Template.twig.
|
|
122
|
-
Template.twig.
|
|
123
|
-
Template.twig.
|
|
124
|
-
Template.twig.
|
|
125
|
-
Template.twig.
|
|
126
|
-
Template.twig.
|
|
127
|
-
Template.twig.
|
|
128
|
-
Template.twig.
|
|
129
|
-
Template.twig.
|
|
130
|
-
Template.twig.
|
|
131
|
-
Template.twig.
|
|
132
|
-
Template.twig.
|
|
133
|
-
Template.twig.
|
|
134
|
-
Template.twig.
|
|
135
|
-
Template.twig.add_filter('form_token', Template.get_form_token_input)
|
|
114
|
+
Template.twig.globals['_'] = _translate
|
|
115
|
+
Template.twig.filters['translate'] = _translate
|
|
116
|
+
Template.twig.globals['RANDOM'] = RANDOM
|
|
117
|
+
Template.twig.globals['json'] = json
|
|
118
|
+
Template.twig.globals['base64encode'] = Template.base64encode
|
|
119
|
+
Template.twig.filters['base64encode'] = Template.base64encode
|
|
120
|
+
Template.twig.filters['detect_image'] = Template.detect_image
|
|
121
|
+
Template.twig.filters['json_encode'] = json.dumps
|
|
122
|
+
Template.twig.globals['json_encode'] = json.dumps
|
|
123
|
+
Template.twig.filters['json_decode'] = Template.json_decode
|
|
124
|
+
Template.twig.globals['json_decode'] = Template.json_decode
|
|
125
|
+
Template.twig.filters['nice_label'] = Template.get_nice_label
|
|
126
|
+
Template.twig.globals['datetime_format'] = Template.datetime_format
|
|
127
|
+
Template.twig.filters['datetime_format'] = Template.datetime_format
|
|
128
|
+
Template.twig.globals['formToken'] = Template.get_form_token
|
|
129
|
+
Template.twig.filters['formToken'] = Template.get_form_token_input
|
|
130
|
+
Template.twig.globals['form_token'] = Template.get_form_token
|
|
131
|
+
Template.twig.filters['form_token'] = Template.get_form_token_input
|
|
136
132
|
debug_level = os.getenv("TINA4_DEBUG_LEVEL", "")
|
|
137
133
|
if Constant.TINA4_LOG_DEBUG in debug_level or Constant.TINA4_LOG_ALL in debug_level:
|
|
138
|
-
Template.twig.
|
|
134
|
+
Template.twig.globals['dump'] = Template.dump
|
|
139
135
|
else:
|
|
140
|
-
Template.twig.
|
|
136
|
+
Template.twig.globals['dump'] = Template.production_dump
|
|
141
137
|
|
|
142
|
-
# Apply any custom filters, globals, tests registered before init
|
|
138
|
+
# Apply any custom filters, globals, tests, and extensions registered before init
|
|
143
139
|
for name, func in Template._custom_filters.items():
|
|
144
|
-
Template.twig.
|
|
140
|
+
Template.twig.filters[name] = func
|
|
145
141
|
for name, value in Template._custom_globals.items():
|
|
146
|
-
Template.twig.
|
|
142
|
+
Template.twig.globals[name] = value
|
|
147
143
|
for name, func in Template._custom_tests.items():
|
|
148
|
-
Template.twig.
|
|
144
|
+
Template.twig.tests[name] = func
|
|
145
|
+
for ext in Template._custom_extensions:
|
|
146
|
+
Template.twig.add_extension(ext)
|
|
149
147
|
|
|
150
148
|
Debug.debug("Twig Initialized on " + path)
|
|
151
149
|
return Template.twig
|
|
152
150
|
|
|
153
151
|
@staticmethod
|
|
154
152
|
def datetime_format(value, format="%H:%M %d-%m-%y"):
|
|
155
|
-
if
|
|
153
|
+
if value.strip().upper() == "NOW":
|
|
156
154
|
value = datetime.now()
|
|
157
155
|
try:
|
|
158
156
|
return value.strftime(format)
|
|
@@ -171,14 +169,14 @@ class Template:
|
|
|
171
169
|
|
|
172
170
|
@staticmethod
|
|
173
171
|
def dump(param):
|
|
174
|
-
param = html.unescape(
|
|
175
|
-
if param is not None:
|
|
172
|
+
param = html.unescape(param)
|
|
173
|
+
if param is not None and not isinstance(param, Undefined):
|
|
176
174
|
def json_serialize(obj):
|
|
177
175
|
if isinstance(obj, (date, datetime)):
|
|
178
176
|
return obj.isoformat()
|
|
179
177
|
if isinstance(obj, Session):
|
|
180
178
|
return obj.session_values
|
|
181
|
-
raise TypeError("Type %s not serializable" % type(obj))
|
|
179
|
+
raise TypeError("Type %s not serializable to Jinja2 template" % type(obj))
|
|
182
180
|
|
|
183
181
|
return "<pre>" + json.dumps(param, indent=True, default=json_serialize) + "</pre>"
|
|
184
182
|
else:
|
|
@@ -186,7 +184,7 @@ class Template:
|
|
|
186
184
|
|
|
187
185
|
@staticmethod
|
|
188
186
|
def base64encode(param):
|
|
189
|
-
value =
|
|
187
|
+
value = base64.b64encode(param.encode('utf-8')).decode('utf-8')
|
|
190
188
|
return value
|
|
191
189
|
|
|
192
190
|
@staticmethod
|
|
@@ -204,9 +202,9 @@ class Template:
|
|
|
204
202
|
def convert_special_types(obj):
|
|
205
203
|
"""
|
|
206
204
|
Recursively convert non-JSON-serializable objects:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
• datetime/date → ISO 8601 string
|
|
206
|
+
• bytes base64 string
|
|
207
|
+
• dict/list/tuple/set → recursively processed
|
|
210
208
|
Safe for deeply nested data (arrays of arrays, dicts in lists, etc.)
|
|
211
209
|
"""
|
|
212
210
|
if isinstance(obj, (date, datetime)):
|
|
@@ -228,7 +226,7 @@ class Template:
|
|
|
228
226
|
]
|
|
229
227
|
|
|
230
228
|
else:
|
|
231
|
-
# Primitives: str, int, float, bool, None
|
|
229
|
+
# Primitives: str, int, float, bool, None → pass through
|
|
232
230
|
return obj
|
|
233
231
|
|
|
234
232
|
@staticmethod
|
|
@@ -243,8 +241,9 @@ class Template:
|
|
|
243
241
|
twig = Template.init_twig(tina4_python.root_path + os.sep + "src" + os.sep + "templates")
|
|
244
242
|
try:
|
|
245
243
|
try:
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
if twig.get_template(template_or_file_name):
|
|
245
|
+
template = twig.get_template(template_or_file_name)
|
|
246
|
+
content = template.render(data)
|
|
248
247
|
except TemplateNotFound:
|
|
249
248
|
template = twig.from_string(template_or_file_name)
|
|
250
249
|
content = template.render(data)
|
|
@@ -261,12 +260,13 @@ class Template:
|
|
|
261
260
|
|
|
262
261
|
@staticmethod
|
|
263
262
|
def get_nice_label(field_name: str) -> str:
|
|
264
|
-
# snake_case / camelCase / PascalCase
|
|
263
|
+
# snake_case / camelCase / PascalCase → words
|
|
265
264
|
s = re.sub(r'[_.-]+', ' ', field_name)
|
|
266
265
|
s = re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', s)
|
|
267
266
|
# Capitalize words & strip id
|
|
268
267
|
words = s.split()
|
|
269
|
-
return " ".join(word.capitalize() for word in words)
|
|
268
|
+
return " ".join(word.capitalize() for word in words )
|
|
269
|
+
|
|
270
270
|
|
|
271
271
|
@staticmethod
|
|
272
272
|
def detect_image(value: Any) -> Dict[str, str]:
|
|
@@ -280,12 +280,12 @@ class Template:
|
|
|
280
280
|
content = data.get('content', '')
|
|
281
281
|
|
|
282
282
|
if not content:
|
|
283
|
-
return
|
|
283
|
+
return {"content": value, "content_type": ""}
|
|
284
284
|
|
|
285
285
|
content_type = data.get('content_type', '')
|
|
286
286
|
if content_type.startswith('image/'):
|
|
287
287
|
mime_type = content_type.split('/')[1]
|
|
288
|
-
return
|
|
288
|
+
return {"content": content, "content_type": mime_type}
|
|
289
289
|
|
|
290
290
|
# Fallback to magic bytes if no content_type
|
|
291
291
|
if content[:4] == '/9j/':
|
|
@@ -297,7 +297,7 @@ class Template:
|
|
|
297
297
|
elif content[:5] == 'UklGR':
|
|
298
298
|
mime_type = 'webp'
|
|
299
299
|
else:
|
|
300
|
-
return
|
|
300
|
+
return {"content": content, "content_type": ""}
|
|
301
301
|
|
|
302
302
|
return {"content": content, "content_type": mime_type}
|
|
303
303
|
except json.JSONDecodeError as e:
|
|
@@ -328,14 +328,14 @@ def template(twig_file: str):
|
|
|
328
328
|
async def wrapper(*args, **kwargs):
|
|
329
329
|
result = await func(*args, **kwargs)
|
|
330
330
|
|
|
331
|
-
# If the route returns a dict
|
|
331
|
+
# If the route returns a dict → render the template
|
|
332
332
|
if isinstance(result, dict):
|
|
333
|
-
|
|
333
|
+
html = Template.render(twig_file, result)
|
|
334
334
|
from tina4_python.Response import Response
|
|
335
|
-
return Response(
|
|
335
|
+
return Response(html, Constant.HTTP_OK, Constant.TEXT_HTML)
|
|
336
336
|
|
|
337
337
|
# Anything else (redirects, JSON, etc.) is passed through unchanged
|
|
338
338
|
return result
|
|
339
339
|
|
|
340
340
|
return wrapper
|
|
341
|
-
return decorator
|
|
341
|
+
return decorator
|
|
@@ -223,9 +223,9 @@ if not os.path.exists(root_path + os.sep + "src" + os.sep + "public"):
|
|
|
223
223
|
shutil.copytree(source_dir, destination_dir)
|
|
224
224
|
|
|
225
225
|
# Declare built-ins so we don't always have to import stuff.
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
226
|
+
# Light imports (no heavy deps) are loaded eagerly.
|
|
227
|
+
# Heavy imports (jwt, jinja2, database drivers) are deferred until first use
|
|
228
|
+
# via module __getattr__ and registered as builtins on first access.
|
|
229
229
|
import builtins
|
|
230
230
|
from .Router import get, post, put, patch, delete, middleware, cached, noauth, secured, wsdl
|
|
231
231
|
from .Testing import tests, assert_equal, assert_raises
|
|
@@ -233,34 +233,57 @@ from .Debug import Debug
|
|
|
233
233
|
from .FieldTypes import IntegerField, StringField, JSONBField, TextField, BlobField, NumericField, DateTimeField
|
|
234
234
|
from .Constant import TEXT_HTML, TEXT_PLAIN, TEXT_CSS, TINA4_POST, TINA4_DELETE, TINA4_ANY, TINA4_PUT, TINA4_PATCH, TINA4_OPTIONS, TINA4_LOG_ALL, TINA4_LOG_WARNING, TINA4_LOG_ERROR, TINA4_LOG_DEBUG, TINA4_GET, TINA4_LOG_INFO, HTTP_OK, HTTP_SERVER_ERROR, HTTP_FORBIDDEN, HTTP_NO_CONTENT, HTTP_PARTIAL_CONTENT, HTTP_CREATED, HTTP_UNAUTHORIZED, HTTP_ACCEPTED, HTTP_REDIRECT, HTTP_REDIRECT_MOVED, HTTP_REDIRECT_OTHER, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, LOOKUP_HTTP_CODE, APPLICATION_JSON, APPLICATION_XML
|
|
235
235
|
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
from .Swagger import Swagger, description, secure, summary, example, example_response, tags, params, describe
|
|
242
|
-
from .GraphQL import GraphQL, GraphQLSchema, GraphQLType
|
|
243
|
-
from .Seeder import FakeData, Seeder, seed_orm, seed_table, seed
|
|
244
|
-
|
|
245
|
-
# Register all as builtins
|
|
246
|
-
for _obj in (get, post, put, patch, delete, middleware, cached, noauth, secured, wsdl,
|
|
247
|
-
tests, assert_equal, assert_raises,
|
|
248
|
-
IntegerField, StringField, JSONBField, TextField, BlobField, NumericField, DateTimeField,
|
|
249
|
-
description, secure, summary, example, example_response, tags, params, describe, template):
|
|
250
|
-
if _obj.__name__ not in builtins.__dict__:
|
|
251
|
-
builtins.__dict__[_obj.__name__] = _obj
|
|
236
|
+
# Register light builtins immediately
|
|
237
|
+
for deco in (get, post, put, patch, delete, middleware, cached, noauth, secured, wsdl, tests, assert_equal, assert_raises,
|
|
238
|
+
IntegerField, StringField, JSONBField, TextField, BlobField, NumericField, DateTimeField):
|
|
239
|
+
if deco.__name__ not in builtins.__dict__:
|
|
240
|
+
builtins.__dict__[deco.__name__] = deco
|
|
252
241
|
|
|
253
242
|
builtins.Debug = Debug
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
243
|
+
|
|
244
|
+
# Lazy module attributes — resolved on `from tina4_python import X` or `tina4_python.X`
|
|
245
|
+
# Maps attribute name → (submodule_path, attribute_name)
|
|
246
|
+
_LAZY_ATTRS = {
|
|
247
|
+
"Database": (".Database", "Database"),
|
|
248
|
+
"ORM": (".ORM", "ORM"),
|
|
249
|
+
"orm": (".ORM", "orm"),
|
|
250
|
+
"Api": (".Api", "Api"),
|
|
251
|
+
"template": (".Template", "template"),
|
|
252
|
+
"description": (".Swagger", "description"),
|
|
253
|
+
"secure": (".Swagger", "secure"),
|
|
254
|
+
"summary": (".Swagger", "summary"),
|
|
255
|
+
"example": (".Swagger", "example"),
|
|
256
|
+
"example_response": (".Swagger", "example_response"),
|
|
257
|
+
"tags": (".Swagger", "tags"),
|
|
258
|
+
"params": (".Swagger", "params"),
|
|
259
|
+
"describe": (".Swagger", "describe"),
|
|
260
|
+
"GraphQL": (".GraphQL", "GraphQL"),
|
|
261
|
+
"GraphQLSchema": (".GraphQL", "GraphQLSchema"),
|
|
262
|
+
"GraphQLType": (".GraphQL", "GraphQLType"),
|
|
263
|
+
"FakeData": (".Seeder", "FakeData"),
|
|
264
|
+
"Seeder": (".Seeder", "Seeder"),
|
|
265
|
+
"seed_orm": (".Seeder", "seed_orm"),
|
|
266
|
+
"seed_table": (".Seeder", "seed_table"),
|
|
267
|
+
"seed": (".Seeder", "seed"),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
def __getattr__(name):
|
|
271
|
+
"""Module-level __getattr__ for lazy imports.
|
|
272
|
+
|
|
273
|
+
Called when ``from tina4_python import X`` or ``tina4_python.X`` is used
|
|
274
|
+
and X isn't already in the module namespace. Loads the real attribute
|
|
275
|
+
from the submodule and caches it in both the module dict and builtins.
|
|
276
|
+
"""
|
|
277
|
+
if name in _LAZY_ATTRS:
|
|
278
|
+
submod, attr = _LAZY_ATTRS[name]
|
|
279
|
+
mod = importlib.import_module(submod, package="tina4_python")
|
|
280
|
+
obj = getattr(mod, attr)
|
|
281
|
+
# Cache in module dict so __getattr__ isn't called again
|
|
282
|
+
globals()[name] = obj
|
|
283
|
+
# Also register as builtin for zero-import convenience
|
|
284
|
+
builtins.__dict__[name] = obj
|
|
285
|
+
return obj
|
|
286
|
+
raise AttributeError(f"module 'tina4_python' has no attribute {name!r}")
|
|
264
287
|
|
|
265
288
|
|
|
266
289
|
# src/ is loaded lazily inside run_web_server() via _autoload_routes()
|
|
@@ -351,10 +374,8 @@ async def get_swagger_json(request, response):
|
|
|
351
374
|
@get(os.getenv("SWAGGER_ROUTE", "/swagger"))
|
|
352
375
|
async def get_swagger(request, response):
|
|
353
376
|
"""Serve interactive Swagger UI page."""
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
swagger_path = library_path + os.sep + "public" + os.sep + "swagger" + os.sep + "index.html"
|
|
357
|
-
html = file_get_contents(swagger_path)
|
|
377
|
+
html = file_get_contents(
|
|
378
|
+
root_path + os.sep + "src" + os.sep + "public" + os.sep + "swagger" + os.sep + "index.html")
|
|
358
379
|
|
|
359
380
|
html = html.replace("{SWAGGER_ROUTE}", os.getenv("SWAGGER_ROUTE", "/swagger"))
|
|
360
381
|
return response(html)
|
|
@@ -579,7 +600,7 @@ def run_web_server(hostname="localhost", port=7145, debug: bool = False):
|
|
|
579
600
|
# ------------------------------------------------------------------
|
|
580
601
|
# Show a clickable URL in the banner (0.0.0.0 isn't useful for devs)
|
|
581
602
|
display_host = "localhost" if hostname in ("0.0.0.0", "::") else hostname
|
|
582
|
-
Debug.info(f"Server started http://{display_host}:{port}
|
|
603
|
+
Debug.info(f"Server started http://{display_host}:{port}")
|
|
583
604
|
webserver(hostname, port, debug=debug) # Pass debug flag down
|
|
584
605
|
|
|
585
606
|
|