tina4-python 0.2.175__tar.gz → 0.2.183__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.175 → tina4_python-0.2.183}/.gitignore +5 -1
- {tina4_python-0.2.175 → tina4_python-0.2.183}/PKG-INFO +1 -1
- {tina4_python-0.2.175 → tina4_python-0.2.183}/pyproject.toml +1 -1
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Api.py +21 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Auth.py +28 -1
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/CLAUDE.md +9 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/CRUD.py +32 -10
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Constant.py +18 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Database.py +49 -13
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/DatabaseResult.py +32 -9
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/DatabaseTypes.py +14 -0
- tina4_python-0.2.183/tina4_python/Debug.py +225 -0
- tina4_python-0.2.183/tina4_python/DevReload.py +482 -0
- tina4_python-0.2.183/tina4_python/Env.py +74 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/FieldTypes.py +29 -0
- tina4_python-0.2.183/tina4_python/HtmlElement.py +346 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Localization.py +19 -0
- tina4_python-0.2.183/tina4_python/MiddleWare.py +169 -0
- tina4_python-0.2.183/tina4_python/ORM.py +819 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Queue.py +163 -8
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Request.py +8 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Response.py +27 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Router.py +341 -52
- tina4_python-0.2.183/tina4_python/Session.py +566 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Template.py +86 -2
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Webserver.py +37 -0
- tina4_python-0.2.183/tina4_python/Websocket.py +102 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/__init__.py +101 -27
- tina4_python-0.2.183/tina4_python/templates/errors/500.twig +26 -0
- tina4_python-0.2.175/tina4_python/Debug.py +0 -119
- tina4_python-0.2.175/tina4_python/Env.py +0 -39
- tina4_python-0.2.175/tina4_python/HtmlElement.py +0 -169
- tina4_python-0.2.175/tina4_python/MiddleWare.py +0 -90
- tina4_python-0.2.175/tina4_python/ORM.py +0 -441
- tina4_python-0.2.175/tina4_python/Session.py +0 -346
- tina4_python-0.2.175/tina4_python/Websocket.py +0 -47
- tina4_python-0.2.175/tina4_python/templates/errors/500.twig +0 -11
- {tina4_python-0.2.175 → tina4_python-0.2.183}/README.md +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Messages.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Migration.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/ShellColors.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Swagger.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Testing.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/WSDL.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/cli.py +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/messages.pot +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/reconnecting-websocket.js +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/tina4helper.js +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
/tests/.env
|
|
28
28
|
/tests/app.py
|
|
29
29
|
/test.db
|
|
30
|
+
*.db-wal
|
|
31
|
+
*.db-shm
|
|
32
|
+
*.db-journal
|
|
33
|
+
test_queue*.db
|
|
34
|
+
/queue.db
|
|
30
35
|
/logs/
|
|
31
36
|
/migrations/__test_user.sql
|
|
32
37
|
/migrations/__test_user_item.sql
|
|
@@ -36,7 +41,6 @@
|
|
|
36
41
|
/publish.bat
|
|
37
42
|
/Dockerfile
|
|
38
43
|
/data.db
|
|
39
|
-
/test_queue.db-shm
|
|
40
44
|
/publish.sh
|
|
41
45
|
/src/scss/
|
|
42
46
|
/broken
|
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""HTTP client for consuming external REST APIs.
|
|
8
|
+
|
|
9
|
+
The ``Api`` class is a lightweight wrapper around ``requests`` that
|
|
10
|
+
simplifies common patterns when calling third-party services from
|
|
11
|
+
Tina4 applications.
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
- Base URL with automatic path joining
|
|
15
|
+
- Bearer token and custom ``Authorization`` header support
|
|
16
|
+
- HTTP Basic authentication
|
|
17
|
+
- Persistent and per-request custom headers
|
|
18
|
+
- Automatic JSON serialisation of request bodies
|
|
19
|
+
- SSL verification toggle
|
|
20
|
+
- Consistent response dict: ``{status_code, data, headers}``
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
api = Api("https://api.example.com", token="sk-...")
|
|
25
|
+
result = api.get("/users")
|
|
26
|
+
user = api.post("/users", {"name": "Alice"})
|
|
27
|
+
"""
|
|
7
28
|
|
|
8
29
|
import json
|
|
9
30
|
from typing import Optional, Dict, Any, Union, List
|
|
@@ -4,6 +4,33 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""JWT authentication and password hashing for Tina4.
|
|
8
|
+
|
|
9
|
+
Provides the ``Auth`` class which handles:
|
|
10
|
+
- RS256 JWT token creation and validation using auto-generated
|
|
11
|
+
self-signed certificates (stored in the ``cert/`` directory)
|
|
12
|
+
- Configurable token expiry (default 24 hours, override via
|
|
13
|
+
``TINA4_TOKEN_EXPIRES_IN`` environment variable)
|
|
14
|
+
- Password hashing and verification using bcrypt
|
|
15
|
+
- Payload extraction from valid or expired tokens
|
|
16
|
+
|
|
17
|
+
The framework creates a global ``tina4_python.tina4_auth`` instance at
|
|
18
|
+
startup. Sessions, secured routes, and middleware all delegate to this
|
|
19
|
+
instance for token operations.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
from tina4_python import tina4_auth
|
|
24
|
+
|
|
25
|
+
token = tina4_auth.get_token({"user_id": 42})
|
|
26
|
+
is_valid = tina4_auth.valid(token)
|
|
27
|
+
payload = tina4_auth.get_payload(token)
|
|
28
|
+
hashed = tina4_auth.get_password("secret")
|
|
29
|
+
ok = tina4_auth.check_password("secret", hashed)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__all__ = ["Auth", "AuthJSONSerializer"]
|
|
33
|
+
|
|
7
34
|
import datetime
|
|
8
35
|
import os
|
|
9
36
|
import jwt
|
|
@@ -271,7 +298,7 @@ class Auth:
|
|
|
271
298
|
try:
|
|
272
299
|
payload = jwt.decode(token, key=public_key, algorithms=["RS256"])
|
|
273
300
|
return payload
|
|
274
|
-
except
|
|
301
|
+
except Exception:
|
|
275
302
|
return None
|
|
276
303
|
|
|
277
304
|
def validate(self, token: str) -> bool:
|
|
@@ -62,6 +62,7 @@ async def create_user(request, response):
|
|
|
62
62
|
- **POST/PUT/PATCH/DELETE** require `Authorization: Bearer <token>`
|
|
63
63
|
- Use `@noauth()` to make a write route public
|
|
64
64
|
- Use `@secured()` to protect a GET route
|
|
65
|
+
- Make sure you use formToken filter in forms when you need to POST data.
|
|
65
66
|
|
|
66
67
|
```python
|
|
67
68
|
from tina4_python.Router import post, noauth, secured
|
|
@@ -105,6 +106,14 @@ from tina4_python.Response import Response
|
|
|
105
106
|
Response.add_header("X-Custom", "value")
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
## Sessions
|
|
110
|
+
|
|
111
|
+
TINA4_TOKEN_LIMIT is used to set the session time, recommend 15-30 minutes
|
|
112
|
+
|
|
113
|
+
### Authentication & Security
|
|
114
|
+
- Use `tina4_python.tina4_auth.hash_password()` to hash passwords — never use hashlib directly.
|
|
115
|
+
- Use `tina4_python.tina4_auth.check_password(hash, password)` to verify passwords.
|
|
116
|
+
|
|
108
117
|
## Templates (Twig)
|
|
109
118
|
|
|
110
119
|
Templates use Jinja2/Twig syntax and live in `src/templates/`.
|
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""Automatic CRUD interface generator for Tina4 database results.
|
|
8
|
+
|
|
9
|
+
The ``CRUD`` class turns any SQL result set into a searchable, paginated
|
|
10
|
+
HTML + JSON interface with automatic RESTful route registration. It is
|
|
11
|
+
the base class of ``DatabaseResult``, so every query result inherits
|
|
12
|
+
these capabilities.
|
|
13
|
+
|
|
14
|
+
Features:
|
|
15
|
+
- Zero-configuration table detection from SELECT queries
|
|
16
|
+
- Automatic route registration (GET, POST, DELETE) for a full CRUD UI
|
|
17
|
+
- Built-in server-side search and pagination
|
|
18
|
+
- Safe JSON serialisation (Decimal, datetime, bytes → base64)
|
|
19
|
+
- Per-table Twig template auto-copy for customisation
|
|
20
|
+
- Works with all supported database drivers
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
result = db.fetch("select * from articles")
|
|
25
|
+
html = result.to_crud(request, {"primary_key": "id", "limit": 25})
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__all__ = ["CRUD"]
|
|
7
29
|
|
|
8
30
|
import base64
|
|
9
31
|
import datetime
|
|
@@ -345,7 +367,7 @@ class CRUD:
|
|
|
345
367
|
except Exception as e:
|
|
346
368
|
return "Error rendering CRUD: "+str(e)
|
|
347
369
|
|
|
348
|
-
def to_array(self,
|
|
370
|
+
def to_array(self, filter_fn=None, base64_encode = True):
|
|
349
371
|
"""
|
|
350
372
|
Convert the internal records to a JSON-serializable list of dictionaries.
|
|
351
373
|
|
|
@@ -355,11 +377,11 @@ class CRUD:
|
|
|
355
377
|
• bytes/memoryview → base64 encoded string
|
|
356
378
|
|
|
357
379
|
Args:
|
|
358
|
-
|
|
380
|
+
filter_fn (callable, optional): Function applied to each record dict before returning.
|
|
359
381
|
|
|
360
382
|
Returns:
|
|
361
383
|
list[dict]: Serializable records.
|
|
362
|
-
:param
|
|
384
|
+
:param filter_fn:
|
|
363
385
|
:param base64_encode: Override default base64 encoding
|
|
364
386
|
"""
|
|
365
387
|
if self.error is not None:
|
|
@@ -379,7 +401,7 @@ class CRUD:
|
|
|
379
401
|
json_record[key] = base64.b64encode(record[key].tobytes()).decode('utf-8')
|
|
380
402
|
else:
|
|
381
403
|
json_record[key] = record[key].tobytes().decode("utf-8")
|
|
382
|
-
elif isinstance(record[key], bytes)
|
|
404
|
+
elif isinstance(record[key], bytes):
|
|
383
405
|
if base64_encode:
|
|
384
406
|
json_record[key] = base64.b64encode(record[key]).decode('utf-8')
|
|
385
407
|
else:
|
|
@@ -387,8 +409,8 @@ class CRUD:
|
|
|
387
409
|
else:
|
|
388
410
|
json_record[key] = record[key]
|
|
389
411
|
|
|
390
|
-
if
|
|
391
|
-
json_record =
|
|
412
|
+
if filter_fn is not None:
|
|
413
|
+
json_record = filter_fn(json_record)
|
|
392
414
|
|
|
393
415
|
json_records.append(json_record)
|
|
394
416
|
|
|
@@ -396,13 +418,13 @@ class CRUD:
|
|
|
396
418
|
else:
|
|
397
419
|
return []
|
|
398
420
|
|
|
399
|
-
def to_list(self,
|
|
421
|
+
def to_list(self, filter_fn=None, base64_encode = True):
|
|
400
422
|
"""Alias of to_array() for readability."""
|
|
401
|
-
return self.to_array(
|
|
423
|
+
return self.to_array(filter_fn, base64_encode=base64_encode)
|
|
402
424
|
|
|
403
|
-
def to_json(self,
|
|
425
|
+
def to_json(self, filter_fn=None, base64_encode = True):
|
|
404
426
|
"""Return records as a JSON encoded string."""
|
|
405
|
-
return json.dumps(self.to_array(
|
|
427
|
+
return json.dumps(self.to_array(filter_fn, base64_encode=base64_encode))
|
|
406
428
|
|
|
407
429
|
def __iter__(self):
|
|
408
430
|
"""Allow iteration over records (yields results from to_array())."""
|
|
@@ -18,6 +18,24 @@ They are intentionally simple strings/integers to allow direct comparison
|
|
|
18
18
|
and use in decorators, routing, and response handling.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Logging levels
|
|
23
|
+
"TINA4_LOG_INFO", "TINA4_LOG_WARNING", "TINA4_LOG_DEBUG",
|
|
24
|
+
"TINA4_LOG_ERROR", "TINA4_LOG_ALL",
|
|
25
|
+
# HTTP methods
|
|
26
|
+
"TINA4_GET", "TINA4_POST", "TINA4_ANY", "TINA4_PATCH",
|
|
27
|
+
"TINA4_PUT", "TINA4_DELETE", "TINA4_OPTIONS",
|
|
28
|
+
# HTTP status codes
|
|
29
|
+
"HTTP_OK", "HTTP_CREATED", "HTTP_ACCEPTED", "HTTP_NO_CONTENT",
|
|
30
|
+
"HTTP_PARTIAL_CONTENT", "HTTP_REDIRECT_MOVED", "HTTP_REDIRECT",
|
|
31
|
+
"HTTP_REDIRECT_OTHER", "HTTP_BAD_REQUEST", "HTTP_UNAUTHORIZED",
|
|
32
|
+
"HTTP_FORBIDDEN", "HTTP_NOT_FOUND", "HTTP_SERVER_ERROR",
|
|
33
|
+
"LOOKUP_HTTP_CODE",
|
|
34
|
+
# MIME types
|
|
35
|
+
"TEXT_HTML", "TEXT_CSS", "TEXT_PLAIN", "TEXT_JAVASCRIPT",
|
|
36
|
+
"APPLICATION_JSON", "APPLICATION_XML",
|
|
37
|
+
]
|
|
38
|
+
|
|
21
39
|
# ----------------------------------------------------------------------
|
|
22
40
|
# Logging Levels
|
|
23
41
|
# ----------------------------------------------------------------------
|
|
@@ -4,6 +4,42 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""Multi-driver database abstraction layer for Tina4.
|
|
8
|
+
|
|
9
|
+
This module provides the ``Database`` class — a unified interface for
|
|
10
|
+
connecting to and querying relational databases. Driver selection is
|
|
11
|
+
automatic based on the connection-string prefix:
|
|
12
|
+
|
|
13
|
+
Supported drivers:
|
|
14
|
+
- ``sqlite`` — SQLite 3 (built-in, file-based)
|
|
15
|
+
- ``mysql`` — MySQL / MariaDB via ``mysql-connector-python``
|
|
16
|
+
- ``postgres`` — PostgreSQL via ``psycopg2``
|
|
17
|
+
- ``firebird`` — Firebird via ``firebirdsql``
|
|
18
|
+
- ``mssql`` — Microsoft SQL Server via ``pymssql``
|
|
19
|
+
- ``odbc`` — Generic ODBC via ``pyodbc``
|
|
20
|
+
|
|
21
|
+
Key features:
|
|
22
|
+
- Connection-string parsing (``driver:host/port:schema``)
|
|
23
|
+
- Parameter-based queries with ``?`` placeholders
|
|
24
|
+
- Built-in pagination via ``fetch(sql, limit, skip)``
|
|
25
|
+
- Full-text search across specified columns
|
|
26
|
+
- CRUD helpers: ``insert``, ``update``, ``delete``
|
|
27
|
+
- Transaction support: ``begin``, ``commit``, ``rollback``
|
|
28
|
+
- Metadata introspection: ``get_database_tables``, ``get_table_info``
|
|
29
|
+
- Automatic JSON/Decimal/datetime serialisation in results
|
|
30
|
+
|
|
31
|
+
Example::
|
|
32
|
+
|
|
33
|
+
from tina4_python.Database import Database
|
|
34
|
+
|
|
35
|
+
db = Database("sqlite:my_app.db")
|
|
36
|
+
result = db.fetch("select * from users", limit=10, skip=0)
|
|
37
|
+
for row in result:
|
|
38
|
+
print(row)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__all__ = ["Database"]
|
|
42
|
+
|
|
7
43
|
import base64
|
|
8
44
|
import os
|
|
9
45
|
import re
|
|
@@ -19,24 +55,24 @@ import datetime
|
|
|
19
55
|
|
|
20
56
|
class Database:
|
|
21
57
|
|
|
22
|
-
def __init__(self,
|
|
58
|
+
def __init__(self, connection_string, username="", password=""):
|
|
23
59
|
"""
|
|
24
60
|
Initializes a database connection
|
|
25
|
-
:param
|
|
61
|
+
:param connection_string:
|
|
26
62
|
"""
|
|
27
63
|
# split out the connection string
|
|
28
64
|
# driver:host/port:schema/path
|
|
29
|
-
params =
|
|
65
|
+
params = connection_string.split(":", 1)
|
|
30
66
|
|
|
31
67
|
try:
|
|
32
|
-
if
|
|
33
|
-
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if
|
|
68
|
+
if connection_string is None:
|
|
69
|
+
connection_string = os.environ.get("DATABASE_PATH", None)
|
|
70
|
+
if username == "":
|
|
71
|
+
username = os.environ.get("DATABASE_USERNAME", "")
|
|
72
|
+
if password == "":
|
|
73
|
+
password = os.environ.get("DATABASE_PASSWORD", "")
|
|
74
|
+
|
|
75
|
+
if connection_string is None:
|
|
40
76
|
raise Exception("Database connection string is missing, try declaring DATABASE_PATH in the .env file.")
|
|
41
77
|
|
|
42
78
|
self.database_module = importlib.import_module(params[0])
|
|
@@ -58,8 +94,8 @@ class Database:
|
|
|
58
94
|
|
|
59
95
|
self.database_engine = params[0]
|
|
60
96
|
self.database_path = params[1]
|
|
61
|
-
self.username =
|
|
62
|
-
self.password =
|
|
97
|
+
self.username = username
|
|
98
|
+
self.password = password
|
|
63
99
|
|
|
64
100
|
if self.database_engine == SQLITE:
|
|
65
101
|
self.dba = self.database_module.connect(self.database_path)
|
|
@@ -4,17 +4,40 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""Database query result container with serialisation and CRUD support.
|
|
8
|
+
|
|
9
|
+
``DatabaseResult`` wraps the rows returned by a database query and extends
|
|
10
|
+
``CRUD`` so that any result set can be converted to JSON, paginated HTML,
|
|
11
|
+
or a full CRUD interface with a single method call.
|
|
12
|
+
|
|
13
|
+
Serialisation formats:
|
|
14
|
+
- ``to_array()`` / ``to_list()`` — Python list of dicts (JSON-safe)
|
|
15
|
+
- ``to_json()`` — JSON string
|
|
16
|
+
- ``to_paginate()`` — dict ready for DataTables-style pagination
|
|
17
|
+
- ``to_csv()`` — CSV text with headers
|
|
18
|
+
- ``to_crud(request, options)`` — full HTML + REST CRUD interface
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
result = db.fetch("select * from products", limit=25)
|
|
23
|
+
print(result.to_json()) # JSON array
|
|
24
|
+
print(result.to_csv()) # CSV text
|
|
25
|
+
paginated = result.to_paginate() # {recordsTotal, data, ...}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__all__ = ["DatabaseResult"]
|
|
29
|
+
|
|
7
30
|
import csv
|
|
8
31
|
import io
|
|
9
32
|
from tina4_python.CRUD import CRUD
|
|
10
33
|
|
|
11
34
|
class DatabaseResult(CRUD):
|
|
12
|
-
def __init__(self,
|
|
35
|
+
def __init__(self, records_list=None, columns=None, error=None, count=None, limit=None, skip=None, sql=None, dba=None):
|
|
13
36
|
"""
|
|
14
37
|
DatabaseResult constructor
|
|
15
|
-
:param
|
|
16
|
-
:param
|
|
17
|
-
:param
|
|
38
|
+
:param records_list:
|
|
39
|
+
:param columns:
|
|
40
|
+
:param error:
|
|
18
41
|
:param count:
|
|
19
42
|
:param limit:
|
|
20
43
|
:param skip:
|
|
@@ -35,15 +58,15 @@ class DatabaseResult(CRUD):
|
|
|
35
58
|
else:
|
|
36
59
|
self.skip = 0
|
|
37
60
|
|
|
38
|
-
if
|
|
39
|
-
self.records =
|
|
61
|
+
if records_list is not None:
|
|
62
|
+
self.records = records_list
|
|
40
63
|
else:
|
|
41
64
|
self.records = []
|
|
42
65
|
|
|
43
66
|
self.count = len(self.records)
|
|
44
67
|
|
|
45
|
-
if
|
|
46
|
-
self.columns =
|
|
68
|
+
if columns is not None:
|
|
69
|
+
self.columns = columns
|
|
47
70
|
else:
|
|
48
71
|
self.columns = []
|
|
49
72
|
|
|
@@ -53,7 +76,7 @@ class DatabaseResult(CRUD):
|
|
|
53
76
|
if dba is not None:
|
|
54
77
|
self.dba = dba
|
|
55
78
|
|
|
56
|
-
self.error =
|
|
79
|
+
self.error = error
|
|
57
80
|
|
|
58
81
|
def to_paginate(self):
|
|
59
82
|
"""
|
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
# License: MIT https://opensource.org/licenses/MIT
|
|
5
5
|
#
|
|
6
6
|
# flake8: noqa: E501
|
|
7
|
+
"""Database driver identifiers and install instructions.
|
|
8
|
+
|
|
9
|
+
Maps short driver names to their Python module paths and provides
|
|
10
|
+
human-readable installation commands for each supported database.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SQLITE",
|
|
15
|
+
"FIREBIRD", "FIREBIRD_INSTALL",
|
|
16
|
+
"MYSQL", "MYSQL_INSTALL",
|
|
17
|
+
"POSTGRES", "POSTGRES_INSTALL",
|
|
18
|
+
"MSSQL", "MSSQL_INSTALL",
|
|
19
|
+
]
|
|
20
|
+
|
|
7
21
|
SQLITE = "sqlite3"
|
|
8
22
|
FIREBIRD = "firebird.driver"
|
|
9
23
|
FIREBIRD_INSTALL = "pip install firebird-driver or poetry add firebird-driver"
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
"""Centralised logging and debug output for Tina4 Python.
|
|
8
|
+
|
|
9
|
+
This module provides a dual-output logging system:
|
|
10
|
+
|
|
11
|
+
- **Console handler** -- coloured output whose verbosity is controlled by the
|
|
12
|
+
``TINA4_DEBUG_LEVEL`` environment variable (default ``All``).
|
|
13
|
+
- **File handler** -- a rotating log file at ``./logs/debug.log`` that always
|
|
14
|
+
captures *all* levels (DEBUG and above), regardless of the console setting.
|
|
15
|
+
|
|
16
|
+
The module exposes a single callable singleton, ``Debug``, which can be used
|
|
17
|
+
either as a function or via its convenience static methods::
|
|
18
|
+
|
|
19
|
+
from tina4_python.Debug import Debug
|
|
20
|
+
|
|
21
|
+
Debug("Server started on port", port) # default INFO level
|
|
22
|
+
Debug.error("Something went wrong:", err) # ERROR level
|
|
23
|
+
Debug.warning("Disk space low") # WARNING level
|
|
24
|
+
Debug.debug("Verbose trace info") # DEBUG level
|
|
25
|
+
|
|
26
|
+
Valid values for the ``TINA4_DEBUG_LEVEL`` env var (case-insensitive):
|
|
27
|
+
``All``, ``Debug``, ``Info``, ``Warning``, ``Error``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__all__ = ["Debug", "setup_logging"]
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import logging
|
|
35
|
+
from logging.handlers import RotatingFileHandler
|
|
36
|
+
|
|
37
|
+
from tina4_python.Constant import (
|
|
38
|
+
TINA4_LOG_ALL,
|
|
39
|
+
TINA4_LOG_DEBUG,
|
|
40
|
+
TINA4_LOG_INFO,
|
|
41
|
+
TINA4_LOG_WARNING,
|
|
42
|
+
TINA4_LOG_ERROR,
|
|
43
|
+
)
|
|
44
|
+
from tina4_python.ShellColors import ShellColors
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ColoredFormatter(logging.Formatter):
|
|
48
|
+
"""Custom logging formatter that applies ANSI colour codes to log output.
|
|
49
|
+
|
|
50
|
+
Each log level is mapped to a colour from ``ShellColors`` so that console
|
|
51
|
+
output is easy to scan visually. Both the message body and the level name
|
|
52
|
+
are wrapped in the appropriate colour escape sequences.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
LEVEL_COLORS (dict): Mapping of ``logging`` level constants to ANSI
|
|
56
|
+
colour escape strings.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
LEVEL_COLORS = {
|
|
60
|
+
logging.DEBUG: ShellColors.green,
|
|
61
|
+
logging.INFO: ShellColors.cyan,
|
|
62
|
+
logging.WARNING: ShellColors.bright_yellow,
|
|
63
|
+
logging.ERROR: ShellColors.bright_red,
|
|
64
|
+
logging.CRITICAL: ShellColors.bright_red + ShellColors.bold,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def format(self, record):
|
|
68
|
+
"""Format a log record with ANSI colour codes.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
record (logging.LogRecord): The log record to format.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: The formatted, colour-coded log string.
|
|
75
|
+
"""
|
|
76
|
+
color = self.LEVEL_COLORS.get(record.levelno, "")
|
|
77
|
+
record.msg = f"{color}{record.msg}{ShellColors.end}"
|
|
78
|
+
record.levelname = f"{color}{record.levelname}{ShellColors.end}"
|
|
79
|
+
return super().format(record)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def setup_logging():
|
|
83
|
+
"""Initialise the ``TINA4`` logger with console and file handlers.
|
|
84
|
+
|
|
85
|
+
This function is idempotent -- if the logger already has handlers
|
|
86
|
+
attached it returns immediately, so it is safe to call multiple times.
|
|
87
|
+
|
|
88
|
+
Behaviour:
|
|
89
|
+
|
|
90
|
+
* Reads the ``TINA4_DEBUG_LEVEL`` environment variable (falling back to
|
|
91
|
+
``All``) and maps it to a Python ``logging`` level for the console handler.
|
|
92
|
+
* Creates a ``StreamHandler`` on ``sys.stdout`` with coloured output via
|
|
93
|
+
``ColoredFormatter``.
|
|
94
|
+
* Creates a ``RotatingFileHandler`` at ``./logs/debug.log`` (5 MB per file,
|
|
95
|
+
10 backups) that always logs at ``DEBUG`` level.
|
|
96
|
+
"""
|
|
97
|
+
logger = logging.getLogger("TINA4")
|
|
98
|
+
if logger.handlers:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
raw = str(os.getenv("TINA4_DEBUG_LEVEL", TINA4_LOG_ALL))
|
|
102
|
+
clean = "".join(c for c in raw if c.isalnum() or c == "_").upper()
|
|
103
|
+
|
|
104
|
+
level_map = {
|
|
105
|
+
"ALL": logging.DEBUG, "TINA4_LOG_ALL": logging.DEBUG,
|
|
106
|
+
"DEBUG": logging.DEBUG, "TINA4_LOG_DEBUG": logging.DEBUG,
|
|
107
|
+
"INFO": logging.INFO, "TINA4_LOG_INFO": logging.INFO,
|
|
108
|
+
"WARN": logging.WARNING, "WARNING": logging.WARNING, "TINA4_LOG_WARNING": logging.WARNING,
|
|
109
|
+
"ERROR": logging.ERROR, "TINA4_LOG_ERROR": logging.ERROR,
|
|
110
|
+
}
|
|
111
|
+
console_level = level_map.get(clean, logging.INFO)
|
|
112
|
+
|
|
113
|
+
# Root logger = console level → blocks lower messages on console
|
|
114
|
+
logger.setLevel(console_level)
|
|
115
|
+
|
|
116
|
+
# Console – respects the level
|
|
117
|
+
console = logging.StreamHandler(sys.stdout)
|
|
118
|
+
console.setLevel(console_level)
|
|
119
|
+
console.setFormatter(ColoredFormatter(
|
|
120
|
+
fmt="%(levelname)-8s: %(asctime)s: %(message)s",
|
|
121
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
122
|
+
))
|
|
123
|
+
logger.addHandler(console)
|
|
124
|
+
|
|
125
|
+
# File – ALWAYS logs everything (DEBUG and above)
|
|
126
|
+
os.makedirs("./logs", exist_ok=True)
|
|
127
|
+
file_handler = RotatingFileHandler(
|
|
128
|
+
"./logs/debug.log",
|
|
129
|
+
maxBytes=5*1024*1024,
|
|
130
|
+
backupCount=10,
|
|
131
|
+
encoding="utf-8"
|
|
132
|
+
)
|
|
133
|
+
file_handler.setLevel(logging.DEBUG)
|
|
134
|
+
file_handler.setFormatter(logging.Formatter(
|
|
135
|
+
"%(levelname)-8s: %(asctime)s: %(message)s"
|
|
136
|
+
))
|
|
137
|
+
logger.addHandler(file_handler)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _Debug:
|
|
141
|
+
"""Callable singleton that wraps the ``TINA4`` logger.
|
|
142
|
+
|
|
143
|
+
Instances of this class act as both a function and an object with static
|
|
144
|
+
convenience methods (``info``, ``error``, ``warning``, ``debug``).
|
|
145
|
+
The module creates a single global instance named ``Debug`` (see bottom
|
|
146
|
+
of module) which is the public API.
|
|
147
|
+
|
|
148
|
+
Examples::
|
|
149
|
+
|
|
150
|
+
Debug("Processing request", request_id) # INFO (default)
|
|
151
|
+
Debug("trace data", data, level=TINA4_LOG_DEBUG) # explicit level
|
|
152
|
+
Debug.error("Unexpected failure:", exc) # static shortcut
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self):
|
|
156
|
+
"""Create the debug wrapper around the ``TINA4`` logger."""
|
|
157
|
+
self.logger = logging.getLogger("TINA4")
|
|
158
|
+
|
|
159
|
+
def __call__(self, *messages, level=TINA4_LOG_INFO):
|
|
160
|
+
"""Log one or more values at the given level.
|
|
161
|
+
|
|
162
|
+
All positional arguments are stringified and joined with spaces.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
*messages: Values to log. Each is converted to ``str`` and
|
|
166
|
+
concatenated with a single space separator.
|
|
167
|
+
level (str): One of the ``TINA4_LOG_*`` constants from
|
|
168
|
+
``tina4_python.Constant``. Defaults to ``TINA4_LOG_INFO``.
|
|
169
|
+
"""
|
|
170
|
+
msg = " ".join(str(m) for m in messages)
|
|
171
|
+
mapping = {
|
|
172
|
+
TINA4_LOG_ALL: self.logger.debug,
|
|
173
|
+
TINA4_LOG_DEBUG: self.logger.debug,
|
|
174
|
+
TINA4_LOG_INFO: self.logger.info,
|
|
175
|
+
TINA4_LOG_WARNING: self.logger.warning,
|
|
176
|
+
TINA4_LOG_ERROR: self.logger.error,
|
|
177
|
+
}
|
|
178
|
+
func = mapping.get(level, self.logger.info)
|
|
179
|
+
func(msg)
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def info(*m):
|
|
183
|
+
"""Log a message at INFO level.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
*m: Values to log. Each is converted to ``str`` and joined
|
|
187
|
+
with a single space.
|
|
188
|
+
"""
|
|
189
|
+
logging.getLogger("TINA4").info(" ".join(str(x) for x in m))
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def error(*m):
|
|
193
|
+
"""Log a message at ERROR level.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
*m: Values to log. Each is converted to ``str`` and joined
|
|
197
|
+
with a single space.
|
|
198
|
+
"""
|
|
199
|
+
logging.getLogger("TINA4").error(" ".join(str(x) for x in m))
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def warning(*m):
|
|
203
|
+
"""Log a message at WARNING level.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
*m: Values to log. Each is converted to ``str`` and joined
|
|
207
|
+
with a single space.
|
|
208
|
+
"""
|
|
209
|
+
logging.getLogger("TINA4").warning(" ".join(str(x) for x in m))
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def debug(*m):
|
|
213
|
+
"""Log a message at DEBUG level.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
*m: Values to log. Each is converted to ``str`` and joined
|
|
217
|
+
with a single space.
|
|
218
|
+
"""
|
|
219
|
+
logging.getLogger("TINA4").debug(" ".join(str(x) for x in m))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Global callable instance — created after class is defined
|
|
223
|
+
Debug = _Debug()
|
|
224
|
+
|
|
225
|
+
__all__ = ["Debug"]
|