tina4-python 0.2.172__tar.gz → 0.2.173__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.172 → tina4_python-0.2.173}/PKG-INFO +1 -1
- {tina4_python-0.2.172 → tina4_python-0.2.173}/pyproject.toml +1 -1
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Auth.py +3 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Database.py +14 -17
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/ORM.py +24 -8
- tina4_python-0.2.173/tina4_python/Request.py +27 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Response.py +35 -53
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Router.py +146 -140
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Session.py +6 -2
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/__init__.py +6 -2
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/cli.py +9 -2
- tina4_python-0.2.172/tina4_python/Request.py +0 -21
- {tina4_python-0.2.172 → tina4_python-0.2.173}/.gitignore +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/README.md +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Api.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/CRUD.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Constant.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/DatabaseResult.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/DatabaseTypes.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Debug.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Env.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/FieldTypes.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Localization.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Messages.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/MiddleWare.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Migration.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Queue.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/ShellColors.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Swagger.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Template.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Testing.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/WSDL.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Webserver.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/Websocket.py +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/messages.pot +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/js/reconnecting-websocket.js +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/js/tina4helper.js +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
|
@@ -152,6 +152,9 @@ class Auth:
|
|
|
152
152
|
"""
|
|
153
153
|
self.root_path = root_path
|
|
154
154
|
self.secret = os.environ.get("SECRET", "{self.secret}")
|
|
155
|
+
if self.secret == "{self.secret}":
|
|
156
|
+
from tina4_python.Debug import Debug
|
|
157
|
+
Debug.warning("No SECRET env var set - using default secret. Set SECRET in your .env for production.")
|
|
155
158
|
self.private_key = os.path.join(root_path, "secrets", "private.key")
|
|
156
159
|
self.public_key = os.path.join(root_path, "secrets", "public.key")
|
|
157
160
|
self.self_signed = os.path.join(root_path, "secrets", "domain.cert")
|
|
@@ -34,7 +34,7 @@ class Database:
|
|
|
34
34
|
if _username == "":
|
|
35
35
|
_username = os.environ.get("DATABASE_USERNAME", "")
|
|
36
36
|
if _password == "":
|
|
37
|
-
_password = os.environ.get("
|
|
37
|
+
_password = os.environ.get("DATABASE_PASSWORD", "")
|
|
38
38
|
|
|
39
39
|
if _connection_string is None:
|
|
40
40
|
raise Exception("Database connection string is missing, try declaring DATABASE_PATH in the .env file.")
|
|
@@ -270,7 +270,7 @@ class Database:
|
|
|
270
270
|
"""
|
|
271
271
|
if params is None:
|
|
272
272
|
params = []
|
|
273
|
-
if params
|
|
273
|
+
if isinstance(params, list):
|
|
274
274
|
params = params.copy()
|
|
275
275
|
else:
|
|
276
276
|
params = list(params)
|
|
@@ -298,8 +298,8 @@ class Database:
|
|
|
298
298
|
cols.append(name)
|
|
299
299
|
|
|
300
300
|
if cols:
|
|
301
|
-
if self.database_engine ==
|
|
302
|
-
like_op = "
|
|
301
|
+
if self.database_engine == POSTGRES:
|
|
302
|
+
like_op = "ILIKE"
|
|
303
303
|
else:
|
|
304
304
|
like_op = "LIKE"
|
|
305
305
|
|
|
@@ -371,7 +371,7 @@ class Database:
|
|
|
371
371
|
finally:
|
|
372
372
|
cursor.close()
|
|
373
373
|
|
|
374
|
-
def fetch_one(self, sql, params=
|
|
374
|
+
def fetch_one(self, sql, params=None, skip=0):
|
|
375
375
|
"""
|
|
376
376
|
Fetch a single record based on a SQL statement, take note that BLOB and byte record data is converted into base64 automatically
|
|
377
377
|
:param str sql: A plain SQL statement or one with params in it designated by ?
|
|
@@ -379,6 +379,8 @@ class Database:
|
|
|
379
379
|
:param int skip: Offset of records to skip
|
|
380
380
|
:return: dict : A dictionary containing the single record
|
|
381
381
|
"""
|
|
382
|
+
if params is None:
|
|
383
|
+
params = []
|
|
382
384
|
# Calling the fetch method with limit as 1 and returning the result
|
|
383
385
|
record = self.fetch(sql, params=params, limit=1, skip=skip)
|
|
384
386
|
if record.error is None and record.count == 1:
|
|
@@ -386,15 +388,14 @@ class Database:
|
|
|
386
388
|
for key in record.records[0]:
|
|
387
389
|
if isinstance(record.records[0][key], Decimal):
|
|
388
390
|
data[key] = float(record.records[0][key])
|
|
389
|
-
|
|
391
|
+
elif isinstance(record.records[0][key], (datetime.date, datetime.datetime)):
|
|
390
392
|
data[key] = record.records[0][key].isoformat()
|
|
391
|
-
|
|
393
|
+
elif isinstance(record.records[0][key], bytes):
|
|
392
394
|
data[key] = base64.b64encode(record.records[0][key]).decode('utf-8')
|
|
395
|
+
elif isinstance(record.records[0][key], str) and self.is_json(record.records[0][key]):
|
|
396
|
+
data[key] = json.loads(record.records[0][key])
|
|
393
397
|
else:
|
|
394
|
-
|
|
395
|
-
data[key] = json.loads(record.records[0][key])
|
|
396
|
-
else:
|
|
397
|
-
data[key] = record.records[0][key]
|
|
398
|
+
data[key] = record.records[0][key]
|
|
398
399
|
return data
|
|
399
400
|
else:
|
|
400
401
|
return None
|
|
@@ -562,8 +563,8 @@ class Database:
|
|
|
562
563
|
|
|
563
564
|
result = None
|
|
564
565
|
for record in data:
|
|
565
|
-
columns = ", ".join(
|
|
566
|
-
placeholders = ", ".join(['?'] * len(
|
|
566
|
+
columns = ", ".join(record.keys())
|
|
567
|
+
placeholders = ", ".join(['?'] * len(record))
|
|
567
568
|
|
|
568
569
|
# Determine if we need to auto-generate the primary key
|
|
569
570
|
need_auto_id = (
|
|
@@ -687,11 +688,7 @@ class Database:
|
|
|
687
688
|
|
|
688
689
|
params = set_values + [pk_value]
|
|
689
690
|
|
|
690
|
-
if self.database_engine == MSSQL:
|
|
691
|
-
self.execute(f"SET IDENTITY_UPDATE {table_name} ON")
|
|
692
691
|
result = self.execute(sql, params)
|
|
693
|
-
if self.database_engine == MSSQL:
|
|
694
|
-
self.execute(f"SET IDENTITY_UPDATE {table_name} OFF")
|
|
695
692
|
if result.error is not None:
|
|
696
693
|
break
|
|
697
694
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
import ast
|
|
7
8
|
import base64
|
|
8
9
|
from datetime import date
|
|
9
10
|
import json
|
|
@@ -17,6 +18,7 @@ def find_all_sub_classes(a_class):
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def orm(dba):
|
|
21
|
+
import importlib
|
|
20
22
|
from tina4_python import root_path
|
|
21
23
|
Debug("Initializing ORM")
|
|
22
24
|
orm_path = root_path + os.sep + "src" + os.sep + "orm"
|
|
@@ -32,7 +34,9 @@ def orm(dba):
|
|
|
32
34
|
# import and set the database object
|
|
33
35
|
try:
|
|
34
36
|
Debug('from src.orm.' + mod_name + ' import ' + mod_name)
|
|
35
|
-
|
|
37
|
+
module = importlib.import_module('src.orm.' + mod_name)
|
|
38
|
+
orm_class = getattr(module, mod_name)
|
|
39
|
+
orm_class.__dba__ = dba
|
|
36
40
|
except Exception as e:
|
|
37
41
|
Debug("Failed to import " + mod_name, str(e))
|
|
38
42
|
classes = find_all_sub_classes(ORM)
|
|
@@ -263,7 +267,7 @@ class ORM:
|
|
|
263
267
|
if order_by: sql += "\norder by " + ", ".join(order_by)
|
|
264
268
|
return sql
|
|
265
269
|
|
|
266
|
-
def fetch_one(self, column_names="*", filter="", params=
|
|
270
|
+
def fetch_one(self, column_names="*", filter="", params=None, join="", group_by="", having="", order_by=""):
|
|
267
271
|
"""
|
|
268
272
|
Fetch one record from the database
|
|
269
273
|
:param column_names:
|
|
@@ -275,10 +279,12 @@ class ORM:
|
|
|
275
279
|
:param order_by:
|
|
276
280
|
:return:
|
|
277
281
|
"""
|
|
282
|
+
if params is None:
|
|
283
|
+
params = []
|
|
278
284
|
sql = self.__build_sql(column_names, join, filter, group_by, having, order_by)
|
|
279
285
|
return self.__dba__.fetch_one(sql, params=params)
|
|
280
286
|
|
|
281
|
-
def fetch(self, column_names="*", filter="", params=
|
|
287
|
+
def fetch(self, column_names="*", filter="", params=None, join="", group_by="", having="", order_by="", limit=10,
|
|
282
288
|
skip=0):
|
|
283
289
|
"""
|
|
284
290
|
Fetch multiple records from the database
|
|
@@ -293,20 +299,24 @@ class ORM:
|
|
|
293
299
|
:param skip:
|
|
294
300
|
:return:
|
|
295
301
|
"""
|
|
302
|
+
if params is None:
|
|
303
|
+
params = []
|
|
296
304
|
sql = self.__build_sql(column_names, join, filter, group_by, having, order_by)
|
|
297
305
|
return self.__dba__.fetch(sql, params=params, limit=limit, skip=skip)
|
|
298
306
|
|
|
299
|
-
def select(self, column_names="*", filter="", params=
|
|
307
|
+
def select(self, column_names="*", filter="", params=None, join="", group_by="", having="", order_by="", limit=10,
|
|
300
308
|
skip=0):
|
|
301
309
|
return self.fetch(column_names, filter, params, join, group_by, having, order_by, limit, skip)
|
|
302
310
|
|
|
303
|
-
def load(self, query="", params=
|
|
311
|
+
def load(self, query="", params=None):
|
|
304
312
|
"""
|
|
305
313
|
Loads a single record into the object based on the primary key or query if query is set
|
|
306
314
|
:param query:
|
|
307
315
|
:param params:
|
|
308
316
|
:return:
|
|
309
317
|
"""
|
|
318
|
+
if params is None:
|
|
319
|
+
params = []
|
|
310
320
|
if not self.__table_exists:
|
|
311
321
|
Debug("ORM: Load Error - Table", self.__table_name__, "does not exist", TINA4_LOG_ERROR)
|
|
312
322
|
return False
|
|
@@ -315,11 +325,13 @@ class ORM:
|
|
|
315
325
|
sql = f"select * from {self.__table_name__} where "
|
|
316
326
|
primary_keys = self.__get_primary_keys()
|
|
317
327
|
values = self.to_dict()
|
|
328
|
+
params = []
|
|
318
329
|
counter = 0
|
|
319
330
|
for key in primary_keys:
|
|
320
331
|
if counter > 0:
|
|
321
332
|
sql += " and "
|
|
322
|
-
sql += key + " =
|
|
333
|
+
sql += key + " = ?"
|
|
334
|
+
params.append(values[key])
|
|
323
335
|
counter += 1
|
|
324
336
|
else:
|
|
325
337
|
sql = f"select * from {self.__table_name__} where {query}"
|
|
@@ -396,7 +408,9 @@ class ORM:
|
|
|
396
408
|
|
|
397
409
|
return result
|
|
398
410
|
|
|
399
|
-
def delete(self, query="", params=
|
|
411
|
+
def delete(self, query="", params=None):
|
|
412
|
+
if params is None:
|
|
413
|
+
params = []
|
|
400
414
|
if not self.__table_exists:
|
|
401
415
|
Debug("ORM: Load Error - Table", self.__table_name__, "does not exist", TINA4_LOG_ERROR)
|
|
402
416
|
return False
|
|
@@ -404,11 +418,13 @@ class ORM:
|
|
|
404
418
|
sql = f"delete from {self.__table_name__} where "
|
|
405
419
|
primary_keys = self.__get_primary_keys()
|
|
406
420
|
values = self.to_dict()
|
|
421
|
+
params = []
|
|
407
422
|
counter = 0
|
|
408
423
|
for key in primary_keys:
|
|
409
424
|
if counter > 0:
|
|
410
425
|
sql += " and "
|
|
411
|
-
sql += key + " =
|
|
426
|
+
sql += key + " = ?"
|
|
427
|
+
params.append(values[key])
|
|
412
428
|
counter += 1
|
|
413
429
|
else:
|
|
414
430
|
sql = f"delete from {self.__table_name__} where {query}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Request:
|
|
10
|
+
"""Per-request object holding all request data. Instantiated fresh for each incoming request."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.request = None
|
|
14
|
+
self.body = None
|
|
15
|
+
self.params = {}
|
|
16
|
+
self.headers = {}
|
|
17
|
+
self.cookies = {}
|
|
18
|
+
self.url = None
|
|
19
|
+
self.session = None
|
|
20
|
+
self.files = {}
|
|
21
|
+
self.raw_request = None
|
|
22
|
+
self.raw_data = None
|
|
23
|
+
self.raw_content = None
|
|
24
|
+
self.asgi_scope = None
|
|
25
|
+
self.asgi_reader = None
|
|
26
|
+
self.asgi_writer = None
|
|
27
|
+
self.asgi_response = None
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import os
|
|
8
8
|
import json
|
|
9
9
|
import inspect
|
|
10
|
+
import contextvars
|
|
10
11
|
from datetime import datetime, date
|
|
11
12
|
from types import ModuleType
|
|
12
13
|
from tina4_python import Constant
|
|
@@ -14,10 +15,9 @@ from tina4_python import DatabaseResult
|
|
|
14
15
|
from tina4_python.ORM import ORM
|
|
15
16
|
from tina4_python.Template import Template
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
content_type = Constant.TEXT_HTML
|
|
18
|
+
# Per-coroutine header accumulation for add_header() calls before Response creation
|
|
19
|
+
_pending_headers = contextvars.ContextVar('_pending_headers', default=None)
|
|
20
|
+
|
|
21
21
|
|
|
22
22
|
class Response:
|
|
23
23
|
|
|
@@ -32,12 +32,14 @@ class Response:
|
|
|
32
32
|
else:
|
|
33
33
|
return obj
|
|
34
34
|
|
|
35
|
+
@staticmethod
|
|
36
|
+
def reset_context():
|
|
37
|
+
"""Reset per-request response state. Called by Router before each request."""
|
|
38
|
+
_pending_headers.set({})
|
|
39
|
+
|
|
35
40
|
def __init__(self, content_in=None, http_code_in=None, content_type_in=None,
|
|
36
41
|
headers_in=None):
|
|
37
|
-
|
|
38
|
-
global content
|
|
39
|
-
global http_code
|
|
40
|
-
global content_type
|
|
42
|
+
content_type = content_type_in if content_type_in is not None else Constant.TEXT_HTML
|
|
41
43
|
|
|
42
44
|
if http_code_in is None:
|
|
43
45
|
http_code_in = Constant.HTTP_OK
|
|
@@ -72,17 +74,19 @@ class Response:
|
|
|
72
74
|
content_in = json.dumps({"error": "Cannot decode object of type " + str(type(content_in))})
|
|
73
75
|
content_type = Constant.APPLICATION_JSON
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
# Merge any headers added via add_header() before this Response was created
|
|
78
|
+
pending = _pending_headers.get()
|
|
79
|
+
if headers_in is not None:
|
|
80
|
+
merged_headers = dict(headers_in)
|
|
81
|
+
elif pending:
|
|
82
|
+
merged_headers = dict(pending)
|
|
83
|
+
else:
|
|
84
|
+
merged_headers = {}
|
|
77
85
|
|
|
78
|
-
self.headers =
|
|
79
|
-
self.content = content_in if content_in is not None else
|
|
80
|
-
self.http_code = http_code_in
|
|
81
|
-
self.content_type =
|
|
82
|
-
headers = self.headers
|
|
83
|
-
http_code = self.http_code
|
|
84
|
-
content_type = self.content_type
|
|
85
|
-
content = self.content
|
|
86
|
+
self.headers = merged_headers
|
|
87
|
+
self.content = content_in if content_in is not None else ""
|
|
88
|
+
self.http_code = http_code_in
|
|
89
|
+
self.content_type = content_type
|
|
86
90
|
|
|
87
91
|
@staticmethod
|
|
88
92
|
def redirect(redirect_url, http_code_in=Constant.HTTP_REDIRECT):
|
|
@@ -92,25 +96,12 @@ class Response:
|
|
|
92
96
|
:param redirect_url:
|
|
93
97
|
:return:
|
|
94
98
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
global http_code
|
|
98
|
-
global content_type
|
|
99
|
-
headers = {}
|
|
100
|
-
http_code = http_code_in
|
|
101
|
-
headers["Location"] = redirect_url
|
|
102
|
-
content = "Redirecting..."
|
|
103
|
-
content_type = Constant.TEXT_HTML
|
|
104
|
-
return Response("Redirecting...", http_code, content_type, headers)
|
|
105
|
-
|
|
99
|
+
headers = {"Location": redirect_url}
|
|
100
|
+
return Response("Redirecting...", http_code_in, Constant.TEXT_HTML, headers)
|
|
106
101
|
|
|
107
102
|
@staticmethod
|
|
108
103
|
def render(template_name, data=None):
|
|
109
|
-
|
|
110
|
-
http_code = Constant.HTTP_OK
|
|
111
|
-
content_type = Constant.TEXT_HTML
|
|
112
|
-
|
|
113
|
-
return Response(Template.render(template_name, data=data), http_code, content_type)
|
|
104
|
+
return Response(Template.render(template_name, data=data), Constant.HTTP_OK, Constant.TEXT_HTML)
|
|
114
105
|
|
|
115
106
|
@staticmethod
|
|
116
107
|
def file(file_path: str, root_path: str = "src/public"):
|
|
@@ -124,24 +115,16 @@ class Response:
|
|
|
124
115
|
Returns:
|
|
125
116
|
Response: A properly configured Response object with file content and correct MIME type
|
|
126
117
|
"""
|
|
127
|
-
global content, content_type, http_code
|
|
128
|
-
|
|
129
118
|
# Resolve full path and prevent directory traversal
|
|
130
119
|
full_path = os.path.abspath(os.path.join(root_path, file_path.lstrip("/")))
|
|
131
120
|
|
|
132
121
|
# Security: ensure the requested file is inside the root_path
|
|
133
122
|
if not full_path.startswith(os.path.abspath(root_path)):
|
|
134
|
-
|
|
135
|
-
content_type = Constant.TEXT_PLAIN
|
|
136
|
-
content = "403 - Forbidden"
|
|
137
|
-
return Response(content, http_code, content_type)
|
|
123
|
+
return Response("403 - Forbidden", Constant.HTTP_FORBIDDEN, Constant.TEXT_PLAIN)
|
|
138
124
|
|
|
139
125
|
# Check if file exists
|
|
140
126
|
if not os.path.isfile(full_path):
|
|
141
|
-
|
|
142
|
-
content_type = Constant.TEXT_PLAIN
|
|
143
|
-
content = "404 - File Not Found"
|
|
144
|
-
return Response(content, http_code, content_type)
|
|
127
|
+
return Response("404 - File Not Found", Constant.HTTP_NOT_FOUND, Constant.TEXT_PLAIN)
|
|
145
128
|
|
|
146
129
|
# Determine MIME type
|
|
147
130
|
extension = os.path.splitext(file_path)[1].lower()
|
|
@@ -173,24 +156,23 @@ class Response:
|
|
|
173
156
|
with open(full_path, "rb") as f:
|
|
174
157
|
content = f.read()
|
|
175
158
|
except Exception as e:
|
|
176
|
-
|
|
177
|
-
content_type = Constant.TEXT_PLAIN
|
|
178
|
-
content = f"Error reading file: {str(e)}"
|
|
179
|
-
return Response(content, http_code, content_type)
|
|
159
|
+
return Response(f"Error reading file: {str(e)}", Constant.HTTP_BAD_REQUEST, Constant.TEXT_PLAIN)
|
|
180
160
|
|
|
181
|
-
|
|
182
|
-
return Response(content, http_code, content_type)
|
|
161
|
+
return Response(content, Constant.HTTP_OK, content_type)
|
|
183
162
|
|
|
184
163
|
@staticmethod
|
|
185
164
|
def add_header(key, value):
|
|
186
165
|
"""
|
|
187
|
-
Adds a header for the response
|
|
166
|
+
Adds a header for the response (concurrency-safe via contextvars)
|
|
188
167
|
:param key:
|
|
189
168
|
:param value:
|
|
190
169
|
:return:
|
|
191
170
|
"""
|
|
192
|
-
|
|
193
|
-
|
|
171
|
+
h = _pending_headers.get()
|
|
172
|
+
if h is None:
|
|
173
|
+
h = {}
|
|
174
|
+
_pending_headers.set(h)
|
|
175
|
+
h[key] = value
|
|
194
176
|
|
|
195
177
|
@staticmethod
|
|
196
178
|
def wsdl(wsdl_instance):
|
|
@@ -38,7 +38,14 @@ class Router:
|
|
|
38
38
|
|
|
39
39
|
@staticmethod
|
|
40
40
|
def clean_url(url):
|
|
41
|
-
"""
|
|
41
|
+
"""Collapse double slashes in a URL path."""
|
|
42
|
+
if not url:
|
|
43
|
+
return "/"
|
|
44
|
+
return re.sub(r'/+', '/', url)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _normalize_url(url):
|
|
48
|
+
"""Full URL normalization: strip query, domain, collapse slashes, add trailing slash."""
|
|
42
49
|
if not url:
|
|
43
50
|
return "/"
|
|
44
51
|
url = url.split('?')[0]
|
|
@@ -54,7 +61,7 @@ class Router:
|
|
|
54
61
|
def get_variables(url, route_path):
|
|
55
62
|
"""Legacy helper - extracts variables with type conversion"""
|
|
56
63
|
variables = {}
|
|
57
|
-
url_path = Router.
|
|
64
|
+
url_path = Router._normalize_url(url).rstrip('/')
|
|
58
65
|
url_segments = [s for s in url_path.strip('/').split('/') if s]
|
|
59
66
|
route_segments = [s for s in route_path.strip('/').split('/') if s]
|
|
60
67
|
|
|
@@ -188,18 +195,17 @@ class Router:
|
|
|
188
195
|
# Renders the URL and returns the content
|
|
189
196
|
@staticmethod
|
|
190
197
|
async def get_result(url, method, request, headers, session):
|
|
191
|
-
from tina4_python import Request
|
|
192
|
-
from tina4_python import Response
|
|
198
|
+
from tina4_python.Request import Request
|
|
199
|
+
from tina4_python.Response import Response
|
|
200
|
+
|
|
201
|
+
# Reset per-request response context (clears pending headers from add_header)
|
|
202
|
+
Response.reset_context()
|
|
193
203
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Response.http_code = Constant.HTTP_NOT_FOUND
|
|
197
|
-
Response.content_type = Constant.TEXT_HTML
|
|
198
|
-
result = Response
|
|
204
|
+
result = None
|
|
205
|
+
route_matched = False
|
|
199
206
|
|
|
200
207
|
Debug.debug("Root Path " + tina4_python.root_path + " " + url, method)
|
|
201
|
-
tina4_python.tina4_current_request
|
|
202
|
-
tina4_python.tina4_current_request["headers"] = headers
|
|
208
|
+
tina4_python.tina4_current_request = {"url": url, "headers": headers}
|
|
203
209
|
|
|
204
210
|
validated = False
|
|
205
211
|
# we can add other methods later but right now we validate gets, posts and other risky methods
|
|
@@ -245,9 +251,8 @@ class Router:
|
|
|
245
251
|
if os.path.isfile(static_file):
|
|
246
252
|
mime_type = mimetypes.guess_type(url)[0]
|
|
247
253
|
with open(static_file, 'rb') as file:
|
|
248
|
-
return Response
|
|
254
|
+
return Response(file.read(), Constant.HTTP_OK, mime_type)
|
|
249
255
|
|
|
250
|
-
old_stdout = None
|
|
251
256
|
buffer = io.StringIO()
|
|
252
257
|
for route in tina4_python.tina4_routes.values():
|
|
253
258
|
if "methods" not in route or method not in route["methods"]:
|
|
@@ -255,33 +260,37 @@ class Router:
|
|
|
255
260
|
|
|
256
261
|
Debug.debug(method, "Matching route ", route['routes'], " to ", url)
|
|
257
262
|
if Router.match(url, route['routes']):
|
|
263
|
+
route_matched = True
|
|
264
|
+
# Snapshot variables immediately to prevent race between concurrent requests
|
|
265
|
+
matched_vars = dict(Router.variables)
|
|
258
266
|
|
|
259
|
-
if
|
|
267
|
+
if not route.get("noauth", False):
|
|
260
268
|
if not validated and Router.requires_auth(route, method, validated):
|
|
261
|
-
return Response
|
|
269
|
+
return Response("Forbidden - Access denied", Constant.HTTP_FORBIDDEN,
|
|
262
270
|
Constant.TEXT_HTML)
|
|
263
271
|
|
|
264
272
|
router_response = route["callback"]
|
|
265
273
|
|
|
266
|
-
# Add the inline variables & construct a Request
|
|
267
|
-
request["params"].update(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
274
|
+
# Add the inline variables & construct a per-request Request object
|
|
275
|
+
request["params"].update(matched_vars)
|
|
276
|
+
|
|
277
|
+
req = Request()
|
|
278
|
+
req.request = request
|
|
279
|
+
req.headers = headers
|
|
280
|
+
req.params = request["params"]
|
|
281
|
+
req.body = request.get("body")
|
|
282
|
+
req.files = request.get("files")
|
|
283
|
+
req.session = session
|
|
284
|
+
req.raw_data = request.get("raw_data")
|
|
285
|
+
req.raw_request = request.get("raw_request")
|
|
286
|
+
req.raw_content = request.get("raw_content")
|
|
287
|
+
req.url = url
|
|
288
|
+
req.asgi_scope = request.get("asgi_scope")
|
|
289
|
+
req.asgi_reader = request.get("asgi_reader")
|
|
290
|
+
req.asgi_writer = request.get("asgi_writer")
|
|
291
|
+
req.asgi_response = request.get("asgi_response")
|
|
292
|
+
|
|
293
|
+
tina4_python.tina4_current_request = req
|
|
285
294
|
|
|
286
295
|
old_stdout = sys.stdout # Memorize the default stdout stream
|
|
287
296
|
sys.stdout = buffer = io.StringIO()
|
|
@@ -289,112 +298,112 @@ class Router:
|
|
|
289
298
|
Constant.HTTP_FORBIDDEN, Constant.HTTP_BAD_REQUEST, Constant.HTTP_UNAUTHORIZED,
|
|
290
299
|
Constant.HTTP_SERVER_ERROR)
|
|
291
300
|
|
|
292
|
-
if "middleware" in route:
|
|
293
|
-
middleware_runner = MiddleWare(route["middleware"]["class"])
|
|
294
|
-
|
|
295
|
-
if "methods" in route["middleware"] and route["middleware"]["methods"] is not None and len(
|
|
296
|
-
route["middleware"]["methods"]) > 0:
|
|
297
|
-
for method in route["middleware"]["methods"]:
|
|
298
|
-
Request, result = middleware_runner.call_direct_method(Request, result, method)
|
|
299
|
-
if result.http_code in error_set:
|
|
300
|
-
return Response.Response(result.content, result.http_code, result.content_type)
|
|
301
|
-
else:
|
|
302
|
-
Request, result = await middleware_runner.call_before_methods(Request, result)
|
|
303
|
-
if result.http_code in error_set:
|
|
304
|
-
return Response.Response(result.content, result.http_code, result.content_type)
|
|
305
|
-
Request, result = await middleware_runner.call_any_methods(Request, result)
|
|
306
|
-
if result.http_code in error_set:
|
|
307
|
-
return Response.Response(result.content, result.http_code, result.content_type)
|
|
308
|
-
|
|
309
301
|
try:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
302
|
+
if "middleware" in route:
|
|
303
|
+
middleware_runner = MiddleWare(route["middleware"]["class"])
|
|
304
|
+
# Pre-route middleware needs a response to pass; create a default
|
|
305
|
+
mw_response = Response("", Constant.HTTP_OK, Constant.TEXT_HTML)
|
|
306
|
+
|
|
307
|
+
if "methods" in route["middleware"] and route["middleware"]["methods"] is not None and len(
|
|
308
|
+
route["middleware"]["methods"]) > 0:
|
|
309
|
+
for mw_method in route["middleware"]["methods"]:
|
|
310
|
+
req, mw_response = middleware_runner.call_direct_method(req, mw_response, mw_method)
|
|
311
|
+
if mw_response.http_code in error_set:
|
|
312
|
+
return Response(mw_response.content, mw_response.http_code, mw_response.content_type)
|
|
313
|
+
else:
|
|
314
|
+
req, mw_response = await middleware_runner.call_before_methods(req, mw_response)
|
|
315
|
+
if mw_response.http_code in error_set:
|
|
316
|
+
return Response(mw_response.content, mw_response.http_code, mw_response.content_type)
|
|
317
|
+
req, mw_response = await middleware_runner.call_any_methods(req, mw_response)
|
|
318
|
+
if mw_response.http_code in error_set:
|
|
319
|
+
return Response(mw_response.content, mw_response.http_code, mw_response.content_type)
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
sig = inspect.signature(router_response)
|
|
323
|
+
kwargs = {}
|
|
324
|
+
|
|
325
|
+
for param_name, param in sig.parameters.items():
|
|
326
|
+
if param_name in matched_vars:
|
|
327
|
+
value = matched_vars[param_name]
|
|
328
|
+
if param.annotation != inspect.Parameter.empty and callable(param.annotation):
|
|
329
|
+
try:
|
|
330
|
+
value = param.annotation(value)
|
|
331
|
+
except ValueError:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Invalid type for path param '{param_name}': expected {param.annotation.__name__}, got '{matched_vars[param_name]}'")
|
|
334
|
+
kwargs[param_name] = value
|
|
335
|
+
elif param_name == 'request':
|
|
336
|
+
kwargs[param_name] = req
|
|
337
|
+
elif param_name == 'response':
|
|
338
|
+
kwargs[param_name] = Response
|
|
339
|
+
elif param.default == inspect.Parameter.empty:
|
|
340
|
+
raise TypeError(f"Missing required parameter: {param_name}")
|
|
341
|
+
|
|
342
|
+
result = await router_response(**kwargs)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
error_msg = tina4_python.global_exception_handler(e)
|
|
345
|
+
tina4_python.container_broken(error_msg)
|
|
346
|
+
if Constant.TINA4_LOG_DEBUG in os.getenv(
|
|
347
|
+
"TINA4_DEBUG_LEVEL") or Constant.TINA4_LOG_ALL in os.getenv("TINA4_DEBUG_LEVEL"):
|
|
348
|
+
html = Template.render_twig_template("errors/500.twig",
|
|
349
|
+
{"server": {"url": url}, "error_message": error_msg})
|
|
350
|
+
return Response(html, Constant.HTTP_SERVER_ERROR, Constant.TEXT_HTML)
|
|
351
|
+
else:
|
|
352
|
+
return Response(error_msg, Constant.HTTP_SERVER_ERROR, Constant.TEXT_HTML)
|
|
353
|
+
|
|
354
|
+
# we have found a result ... make sure we reflect this if the user didn't actually put the correct http response code in
|
|
355
|
+
if result is not None:
|
|
356
|
+
if result.http_code == Constant.HTTP_NOT_FOUND:
|
|
357
|
+
result.http_code = Constant.HTTP_OK
|
|
358
|
+
|
|
359
|
+
if result is not None and "middleware" in route:
|
|
360
|
+
middleware_runner = MiddleWare(route["middleware"]["class"])
|
|
361
|
+
|
|
362
|
+
if "methods" in route["middleware"] and route["middleware"]["methods"] is not None and len(
|
|
363
|
+
route["middleware"]["methods"]) > 0:
|
|
364
|
+
for mw_method in route["middleware"]["methods"]:
|
|
365
|
+
req, result = middleware_runner.call_direct_method(req, result, mw_method)
|
|
366
|
+
if result.http_code in error_set:
|
|
367
|
+
return Response(result.content, result.http_code, result.content_type)
|
|
368
|
+
else:
|
|
369
|
+
req, result = await middleware_runner.call_any_methods(req, result)
|
|
370
|
+
if result.http_code in error_set:
|
|
371
|
+
return Response(result.content, result.http_code, result.content_type)
|
|
349
372
|
|
|
350
|
-
|
|
351
|
-
route["middleware"]["methods"]) > 0:
|
|
352
|
-
for method in route["middleware"]["methods"]:
|
|
353
|
-
Request, result = middleware_runner.call_direct_method(Request, result, method)
|
|
373
|
+
req, result = await middleware_runner.call_after_methods(req, result)
|
|
354
374
|
if result.http_code in error_set:
|
|
355
|
-
return Response
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if "cache" in route and route["cache"] is not None:
|
|
368
|
-
if not route["cache"]["cached"]:
|
|
369
|
-
result.headers["Cache-Control"] = "max-age=1, must-revalidate"
|
|
370
|
-
result.headers["Pragma"] = "no-cache"
|
|
375
|
+
return Response(result.content, result.http_code, result.content_type)
|
|
376
|
+
|
|
377
|
+
if result is not None:
|
|
378
|
+
result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
|
|
379
|
+
if "cache" in route and route["cache"] is not None:
|
|
380
|
+
if not route["cache"]["cached"]:
|
|
381
|
+
result.headers["Cache-Control"] = "max-age=1, must-revalidate"
|
|
382
|
+
result.headers["Pragma"] = "no-cache"
|
|
383
|
+
else:
|
|
384
|
+
result.headers["Cache-Control"] = "max-age=" + str(
|
|
385
|
+
route["cache"]["max_age"]) + ", must-revalidate"
|
|
386
|
+
result.headers["Pragma"] = "cache"
|
|
371
387
|
else:
|
|
372
|
-
result.headers["Cache-Control"] = "max-age
|
|
373
|
-
route["cache"]["max_age"]) + ", must-revalidate"
|
|
388
|
+
result.headers["Cache-Control"] = "max-age=-1, must-revalidate"
|
|
374
389
|
result.headers["Pragma"] = "cache"
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
result.headers["Pragma"] = "cache"
|
|
390
|
+
finally:
|
|
391
|
+
sys.stdout = old_stdout
|
|
378
392
|
|
|
379
393
|
break
|
|
380
394
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if buffer.getvalue() != "":
|
|
395
|
+
# Callback returned None but captured stdout output
|
|
396
|
+
if result is None and route_matched:
|
|
397
|
+
output = buffer.getvalue()
|
|
398
|
+
if output:
|
|
399
|
+
fresh_headers = {"FreshToken": tina4_python.tina4_auth.get_token({"path": url})}
|
|
387
400
|
try:
|
|
388
|
-
return Response
|
|
389
|
-
result.headers)
|
|
401
|
+
return Response(json.loads(output), Constant.HTTP_OK, Constant.APPLICATION_JSON, fresh_headers)
|
|
390
402
|
except Exception:
|
|
391
|
-
return Response
|
|
392
|
-
else:
|
|
393
|
-
result = Response
|
|
394
|
-
result.http_code = Constant.HTTP_NOT_FOUND
|
|
403
|
+
return Response(output, Constant.HTTP_OK, Constant.TEXT_HTML, fresh_headers)
|
|
395
404
|
|
|
396
|
-
# If no route is
|
|
397
|
-
if result
|
|
405
|
+
# If no route matched or result is still None, try twig templates then 404
|
|
406
|
+
if result is None:
|
|
398
407
|
# Serve twigs if the files exist
|
|
399
408
|
twig_files = []
|
|
400
409
|
if url == "/":
|
|
@@ -410,17 +419,19 @@ class Router:
|
|
|
410
419
|
tina4_python.root_path + os.sep + "src" + os.sep + "templates" + os.sep + twig_file,
|
|
411
420
|
)
|
|
412
421
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
422
|
+
twig_headers = {
|
|
423
|
+
"FreshToken": tina4_python.tina4_auth.get_token({"path": url}),
|
|
424
|
+
"Cache-Control": "max-age=-1, public",
|
|
425
|
+
"Pragma": "no-cache"
|
|
426
|
+
}
|
|
416
427
|
content = Template.render_twig_template(twig_file, {"request": tina4_python.tina4_current_request})
|
|
417
428
|
if content != "":
|
|
418
|
-
return Response
|
|
429
|
+
return Response(content, Constant.HTTP_OK, Constant.TEXT_HTML, twig_headers)
|
|
419
430
|
|
|
420
|
-
if result
|
|
431
|
+
if result is None:
|
|
421
432
|
content = Template.render_twig_template(
|
|
422
433
|
"errors/404.twig", {"server": {"url": url}})
|
|
423
|
-
return Response
|
|
434
|
+
return Response(content, Constant.HTTP_NOT_FOUND, Constant.TEXT_HTML)
|
|
424
435
|
|
|
425
436
|
result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
|
|
426
437
|
return result
|
|
@@ -431,11 +442,6 @@ class Router:
|
|
|
431
442
|
Debug.debug(method, "Resolving URL: " + url)
|
|
432
443
|
return await Router.get_result(url, method, request, headers, session)
|
|
433
444
|
|
|
434
|
-
# cleans the url of double slashes
|
|
435
|
-
@staticmethod
|
|
436
|
-
def clean_url(url):
|
|
437
|
-
return url.replace('//', '/')
|
|
438
|
-
|
|
439
445
|
# adds a route to the router
|
|
440
446
|
@staticmethod
|
|
441
447
|
def add(method, route, callback):
|
|
@@ -271,8 +271,12 @@ class Session:
|
|
|
271
271
|
self.session_path = _default_path
|
|
272
272
|
self.session_values = {}
|
|
273
273
|
self.session_hash = ""
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
_handlers = {
|
|
275
|
+
"SessionFileHandler": SessionFileHandler,
|
|
276
|
+
"SessionRedisHandler": SessionRedisHandler,
|
|
277
|
+
"SessionValkeyHandler": SessionValkeyHandler,
|
|
278
|
+
}
|
|
279
|
+
self.default_handler = _handlers.get(_default_handler, SessionFileHandler)
|
|
276
280
|
|
|
277
281
|
def start(self, _hash=None):
|
|
278
282
|
# create a file for the session?
|
|
@@ -243,7 +243,11 @@ builtins.orm = orm
|
|
|
243
243
|
# Auto-import everything from src folders
|
|
244
244
|
if os.path.exists(root_path + os.sep + "src"):
|
|
245
245
|
try:
|
|
246
|
-
|
|
246
|
+
import importlib
|
|
247
|
+
src_module = importlib.import_module("src")
|
|
248
|
+
for attr in dir(src_module):
|
|
249
|
+
if not attr.startswith("_"):
|
|
250
|
+
globals()[attr] = getattr(src_module, attr)
|
|
247
251
|
Debug.info("Initializing src folder")
|
|
248
252
|
except ImportError as e:
|
|
249
253
|
Debug.error("Cannot import src folder", str(e))
|
|
@@ -594,7 +598,7 @@ def _has_control_methods():
|
|
|
594
598
|
if "tina4-python" in file_path:
|
|
595
599
|
return True
|
|
596
600
|
|
|
597
|
-
if "tina4" in file_path or "uvicorn" in file_path
|
|
601
|
+
if "tina4" in file_path or "uvicorn" in file_path or "hypercorn" in file_path:
|
|
598
602
|
return True
|
|
599
603
|
|
|
600
604
|
if not file_path or not file_path.endswith('.py'):
|
|
@@ -56,13 +56,20 @@ def create_project(project_name: str) -> None:
|
|
|
56
56
|
print(f"Creating project in {project_path}")
|
|
57
57
|
|
|
58
58
|
app_content = '''\
|
|
59
|
+
import os
|
|
60
|
+
import tina4_python
|
|
61
|
+
from tina4_python import Debug
|
|
59
62
|
from tina4_python import run_web_server
|
|
60
63
|
from tina4_python.Router import get
|
|
61
64
|
|
|
65
|
+
|
|
62
66
|
@get("/health-check")
|
|
63
|
-
async def
|
|
67
|
+
async def get_healthcheck(request, response):
|
|
68
|
+
if os.path.isfile(tina4_python.root_path + "/broken"):
|
|
69
|
+
Debug.error("broken", tina4_python.root_path + "/broken")
|
|
70
|
+
return response("Broken", 503)
|
|
71
|
+
return response("OK")
|
|
64
72
|
|
|
65
|
-
return response(f"OK")
|
|
66
73
|
|
|
67
74
|
if __name__ == "__main__":
|
|
68
75
|
run_web_server("0.0.0.0", 7145)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Tina4 - This is not a 4ramework.
|
|
3
|
-
# Copy-right 2007 - current Tina4
|
|
4
|
-
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
-
#
|
|
6
|
-
# flake8: noqa: E501
|
|
7
|
-
request = None
|
|
8
|
-
body = None
|
|
9
|
-
params = {}
|
|
10
|
-
headers = {}
|
|
11
|
-
cookies = {}
|
|
12
|
-
url = None
|
|
13
|
-
session = None
|
|
14
|
-
files = {}
|
|
15
|
-
raw_request = None
|
|
16
|
-
raw_data = None
|
|
17
|
-
raw_content = None
|
|
18
|
-
asgi_scope = None
|
|
19
|
-
asgi_reader = None
|
|
20
|
-
asgi_writer = None
|
|
21
|
-
asgi_response = None
|
|
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-0.2.172 → tina4_python-0.2.173}/tina4_python/public/js/reconnecting-websocket.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-0.2.172 → tina4_python-0.2.173}/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-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.172 → tina4_python-0.2.173}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|