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.
Files changed (66) hide show
  1. {tina4_python-0.2.175 → tina4_python-0.2.183}/.gitignore +5 -1
  2. {tina4_python-0.2.175 → tina4_python-0.2.183}/PKG-INFO +1 -1
  3. {tina4_python-0.2.175 → tina4_python-0.2.183}/pyproject.toml +1 -1
  4. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Api.py +21 -0
  5. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Auth.py +28 -1
  6. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/CLAUDE.md +9 -0
  7. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/CRUD.py +32 -10
  8. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Constant.py +18 -0
  9. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Database.py +49 -13
  10. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/DatabaseResult.py +32 -9
  11. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/DatabaseTypes.py +14 -0
  12. tina4_python-0.2.183/tina4_python/Debug.py +225 -0
  13. tina4_python-0.2.183/tina4_python/DevReload.py +482 -0
  14. tina4_python-0.2.183/tina4_python/Env.py +74 -0
  15. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/FieldTypes.py +29 -0
  16. tina4_python-0.2.183/tina4_python/HtmlElement.py +346 -0
  17. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Localization.py +19 -0
  18. tina4_python-0.2.183/tina4_python/MiddleWare.py +169 -0
  19. tina4_python-0.2.183/tina4_python/ORM.py +819 -0
  20. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Queue.py +163 -8
  21. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Request.py +8 -0
  22. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Response.py +27 -0
  23. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Router.py +341 -52
  24. tina4_python-0.2.183/tina4_python/Session.py +566 -0
  25. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Template.py +86 -2
  26. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Webserver.py +37 -0
  27. tina4_python-0.2.183/tina4_python/Websocket.py +102 -0
  28. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/__init__.py +101 -27
  29. tina4_python-0.2.183/tina4_python/templates/errors/500.twig +26 -0
  30. tina4_python-0.2.175/tina4_python/Debug.py +0 -119
  31. tina4_python-0.2.175/tina4_python/Env.py +0 -39
  32. tina4_python-0.2.175/tina4_python/HtmlElement.py +0 -169
  33. tina4_python-0.2.175/tina4_python/MiddleWare.py +0 -90
  34. tina4_python-0.2.175/tina4_python/ORM.py +0 -441
  35. tina4_python-0.2.175/tina4_python/Session.py +0 -346
  36. tina4_python-0.2.175/tina4_python/Websocket.py +0 -47
  37. tina4_python-0.2.175/tina4_python/templates/errors/500.twig +0 -11
  38. {tina4_python-0.2.175 → tina4_python-0.2.183}/README.md +0 -0
  39. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Messages.py +0 -0
  40. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Migration.py +0 -0
  41. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/ShellColors.py +0 -0
  42. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Swagger.py +0 -0
  43. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/Testing.py +0 -0
  44. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/WSDL.py +0 -0
  45. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/cli.py +0 -0
  46. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/messages.pot +0 -0
  47. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/css/readme.md +0 -0
  48. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/favicon.ico +0 -0
  49. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/403.png +0 -0
  50. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/404.png +0 -0
  51. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/500.png +0 -0
  52. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/logo.png +0 -0
  53. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/images/readme.md +0 -0
  54. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/readme.md +0 -0
  55. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  56. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/js/tina4helper.js +0 -0
  57. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/swagger/index.html +0 -0
  58. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  59. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/components/crud.twig +0 -0
  60. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/errors/403.twig +0 -0
  61. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/errors/404.twig +0 -0
  62. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/templates/readme.md +0 -0
  63. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  64. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  65. {tina4_python-0.2.175 → tina4_python-0.2.183}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  66. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 0.2.175
3
+ Version: 0.2.183
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "0.2.175"
3
+ version = "0.2.183"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -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 jwt.InvalidSignatureError:
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, _filter=None, base64_encode = True):
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
- _filter (callable, optional): Function applied to each record dict before returning.
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 _filter:
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) and base64_encode:
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 _filter is not None:
391
- json_record = _filter(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, _filter=None, base64_encode = True):
421
+ def to_list(self, filter_fn=None, base64_encode = True):
400
422
  """Alias of to_array() for readability."""
401
- return self.to_array(_filter, base64_encode=base64_encode)
423
+ return self.to_array(filter_fn, base64_encode=base64_encode)
402
424
 
403
- def to_json(self, _filter=None, base64_encode = True):
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(_filter, base64_encode=base64_encode))
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, _connection_string, _username="", _password=""):
58
+ def __init__(self, connection_string, username="", password=""):
23
59
  """
24
60
  Initializes a database connection
25
- :param _connection_string:
61
+ :param connection_string:
26
62
  """
27
63
  # split out the connection string
28
64
  # driver:host/port:schema/path
29
- params = _connection_string.split(":", 1)
65
+ params = connection_string.split(":", 1)
30
66
 
31
67
  try:
32
- if _connection_string is None:
33
- _connection_string = os.environ.get("DATABASE_PATH", None)
34
- if _username == "":
35
- _username = os.environ.get("DATABASE_USERNAME", "")
36
- if _password == "":
37
- _password = os.environ.get("DATABASE_PASSWORD", "")
38
-
39
- if _connection_string is None:
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 = _username
62
- self.password = _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, _records=None, _columns=None, _error=None, count=None, limit=None, skip=None, sql=None, dba=None):
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 _records:
16
- :param _columns:
17
- :param _error:
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 _records is not None:
39
- self.records = _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 _columns is not None:
46
- self.columns = _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 = _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"]