tina4-python 3.3.0__tar.gz → 3.6.0__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 (135) hide show
  1. {tina4_python-3.3.0 → tina4_python-3.6.0}/PKG-INFO +5 -5
  2. {tina4_python-3.3.0 → tina4_python-3.6.0}/README.md +4 -4
  3. {tina4_python-3.3.0 → tina4_python-3.6.0}/pyproject.toml +1 -1
  4. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/CLAUDE.md +6 -7
  5. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/auth/__init__.py +81 -15
  7. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/request.py +1 -0
  8. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/response.py +4 -0
  9. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/crud/__init__.py +40 -0
  10. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/adapter.py +1 -1
  11. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/connection.py +5 -4
  12. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/firebird.py +3 -3
  13. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/mssql.py +2 -2
  14. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/mysql.py +2 -2
  15. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/odbc.py +3 -3
  16. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/postgres.py +2 -2
  17. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/sqlite.py +2 -2
  18. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/engine.py +3 -3
  19. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +4 -4
  20. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/i18n/__init__.py +3 -1
  21. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/fields.py +1 -1
  22. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/model.py +17 -17
  23. tina4_python-3.6.0/tina4_python/public/js/tina4js.min.js +47 -0
  24. {tina4_python-3.3.0 → tina4_python-3.6.0}/.gitignore +0 -0
  25. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/HtmlElement.py +0 -0
  26. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/Testing.py +0 -0
  27. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/ai/__init__.py +0 -0
  28. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/api/__init__.py +0 -0
  29. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/cache/__init__.py +0 -0
  30. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/cli/__init__.py +0 -0
  31. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/container/__init__.py +0 -0
  32. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/__init__.py +0 -0
  33. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/cache.py +0 -0
  34. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/constants.py +0 -0
  35. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/events.py +0 -0
  36. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/middleware.py +0 -0
  37. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/router.py +0 -0
  38. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/server.py +0 -0
  39. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/__init__.py +0 -0
  40. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/debug/__init__.py +0 -0
  41. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/debug/error_overlay.py +0 -0
  42. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dev_admin/__init__.py +0 -0
  43. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dev_reload.py +0 -0
  44. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/meta.json +0 -0
  48. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/database/meta.json +0 -0
  49. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  50. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
  51. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  52. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/meta.json +0 -0
  53. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  54. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  55. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/meta.json +0 -0
  56. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  57. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/meta.json +0 -0
  58. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  59. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/meta.json +0 -0
  60. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  61. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  62. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/graphql/__init__.py +0 -0
  63. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/messenger/__init__.py +0 -0
  64. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/migration/__init__.py +0 -0
  65. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/migration/runner.py +0 -0
  66. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/__init__.py +0 -0
  67. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.css +0 -0
  68. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.min.css +0 -0
  69. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/favicon.ico +0 -0
  70. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/images/logo.svg +0 -0
  71. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  72. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/frond.min.js +0 -0
  73. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  74. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4.min.js +0 -0
  75. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/swagger/index.html +0 -0
  76. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  77. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue/__init__.py +0 -0
  78. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/__init__.py +0 -0
  79. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
  80. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
  81. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  82. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/__init__.py +0 -0
  83. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  84. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
  85. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  86. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
  87. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
  88. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
  89. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
  90. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
  91. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
  92. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
  93. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
  94. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  95. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
  96. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/base.scss +0 -0
  97. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/colors.scss +0 -0
  98. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
  99. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/seeder/__init__.py +0 -0
  100. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/service/__init__.py +0 -0
  101. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session/__init__.py +0 -0
  102. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/__init__.py +0 -0
  103. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  104. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/redis_handler.py +0 -0
  105. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
  106. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/swagger/__init__.py +0 -0
  107. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/components/crud.twig +0 -0
  108. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  109. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  110. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
  111. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  112. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/302.twig +0 -0
  113. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/401.twig +0 -0
  114. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/403.twig +0 -0
  115. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/404.twig +0 -0
  116. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/500.twig +0 -0
  117. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/502.twig +0 -0
  118. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/503.twig +0 -0
  119. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/base.twig +0 -0
  120. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/frontend/README.md +0 -0
  121. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/readme.md +0 -0
  122. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  123. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  124. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  125. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  126. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  127. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  128. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/websocket/__init__.py +0 -0
  135. {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.3.0
3
+ Version: 3.6.0
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -412,7 +412,7 @@ db = Database("firebird://SYSDBA:masterkey@localhost:3050//path/to/db")
412
412
  db = Database("mongodb://localhost:27017/mydb")
413
413
  db = Database("odbc://DSN=mydsn")
414
414
 
415
- result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, skip=0)
415
+ result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, offset=0)
416
416
  row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
417
417
  db.insert("users", {"name": "Alice", "email": "alice@test.com"})
418
418
  db.commit()
@@ -440,8 +440,8 @@ async def protected(request, response):
440
440
  from tina4_python.auth import Auth
441
441
 
442
442
  auth = Auth(secret="your-secret")
443
- token = auth.create_token({"user_id": 42})
444
- payload = auth.validate_token(token)
443
+ token = auth.get_token({"user_id": 42})
444
+ payload = auth.valid_token(token)
445
445
  ```
446
446
 
447
447
  POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `@noauth()` to make public, `@secured()` to protect GET routes.
@@ -699,7 +699,7 @@ SECRET=your-jwt-secret
699
699
  DATABASE_URL=sqlite:///data/app.db
700
700
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
701
701
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
702
- TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
702
+ TINA4_LOCALE=en # en, fr, af, zh, ja, es
703
703
  TINA4_SESSION_HANDLER=SessionFileHandler
704
704
  SWAGGER_TITLE=My API
705
705
  ```
@@ -380,7 +380,7 @@ db = Database("firebird://SYSDBA:masterkey@localhost:3050//path/to/db")
380
380
  db = Database("mongodb://localhost:27017/mydb")
381
381
  db = Database("odbc://DSN=mydsn")
382
382
 
383
- result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, skip=0)
383
+ result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, offset=0)
384
384
  row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
385
385
  db.insert("users", {"name": "Alice", "email": "alice@test.com"})
386
386
  db.commit()
@@ -408,8 +408,8 @@ async def protected(request, response):
408
408
  from tina4_python.auth import Auth
409
409
 
410
410
  auth = Auth(secret="your-secret")
411
- token = auth.create_token({"user_id": 42})
412
- payload = auth.validate_token(token)
411
+ token = auth.get_token({"user_id": 42})
412
+ payload = auth.valid_token(token)
413
413
  ```
414
414
 
415
415
  POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `@noauth()` to make public, `@secured()` to protect GET routes.
@@ -667,7 +667,7 @@ SECRET=your-jwt-secret
667
667
  DATABASE_URL=sqlite:///data/app.db
668
668
  TINA4_DEBUG=true # Enable dev toolbar, error overlay
669
669
  TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
670
- TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
670
+ TINA4_LOCALE=en # en, fr, af, zh, ja, es
671
671
  TINA4_SESSION_HANDLER=SessionFileHandler
672
672
  SWAGGER_TITLE=My API
673
673
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.3.0"
3
+ version = "3.6.0"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -221,7 +221,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
221
221
  |------|--------------------------------|
222
222
  | Background jobs / async work | `Queue` from `tina4_python.queue` (use `queue.push()`, `queue.consume()`) |
223
223
  | HTTP calls to external APIs | `Api` from `tina4_python.api` |
224
- | JWT tokens & auth | `Auth` from `tina4_python.auth` (create_token, validate_token, get_payload) |
224
+ | JWT tokens & auth | `Auth` from `tina4_python.auth` (get_token, valid_token, get_payload) |
225
225
  | Password hashing | `Auth.hash_password()` / `Auth.check_password()` from `tina4_python.auth` |
226
226
  | Session management | `Session` from `tina4_python.session` |
227
227
  | Database queries & CRUD | `Database` from `tina4_python.database` |
@@ -480,7 +480,7 @@ Response.add_header("X-Custom", "value")
480
480
 
481
481
  ## Sessions
482
482
 
483
- TINA4_TOKEN_LIMIT is used to set the session time, recommend 15-30 minutes
483
+ TINA4_TOKEN_EXPIRES_IN is used to set the session time, recommend 15-60 minutes
484
484
 
485
485
  ### Session Backends
486
486
 
@@ -775,7 +775,7 @@ row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
775
775
  # Result methods
776
776
  result.to_json() # JSON string
777
777
  result.to_array() # List of dicts
778
- result.to_paginate() # Dict with records, count, limit, skip
778
+ result.to_paginate() # Dict with records, count, limit, offset
779
779
  result.to_csv() # CSV string
780
780
 
781
781
  # Transactions
@@ -1185,7 +1185,7 @@ Tina4 v3 supports translations via JSON files in `src/locales/`.
1185
1185
 
1186
1186
  Set the language in `.env`:
1187
1187
  ```bash
1188
- TINA4_LANGUAGE=en # Default locale
1188
+ TINA4_LOCALE=en # Default locale
1189
1189
  TINA4_LOCALE_DIR=src/locales # Directory for translation files
1190
1190
  ```
1191
1191
 
@@ -1516,8 +1516,7 @@ Key `.env` settings:
1516
1516
  # Authentication
1517
1517
  SECRET=your-jwt-secret # JWT signing (default uses insecure placeholder)
1518
1518
  API_KEY=your-api-key # Static bearer token for API auth
1519
- TINA4_TOKEN_LIMIT=2 # Token lifetime in minutes (default: 2)
1520
- TINA4_TOKEN_EXPIRES_IN=2 # Alternative token expiry setting
1519
+ TINA4_TOKEN_EXPIRES_IN=60 # Token lifetime in minutes (default: 60)
1521
1520
 
1522
1521
  # Database
1523
1522
  DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
@@ -1527,7 +1526,7 @@ DATABASE_PASSWORD= # DB password
1527
1526
  # Framework
1528
1527
  TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error overlay)
1529
1528
  TINA4_LOG_LEVEL=ALL # Log verbosity: ALL, DEBUG, INFO, WARNING, ERROR
1530
- TINA4_LANGUAGE=en # Language for framework messages (en, fr, af, zh, ja, es)
1529
+ TINA4_LOCALE=en # Language for framework messages (en, fr, af, zh, ja, es)
1531
1530
  TINA4_DEFAULT_WEBSERVER=FALSE # Set to TRUE to use Tina4's built-in webserver instead of ASGI
1532
1531
  HOST_NAME=localhost:7145
1533
1532
 
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.3.0"
11
+ __version__ = "3.6.0"
12
12
 
13
13
  # ── HTTP Constants ──
14
14
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -10,8 +10,8 @@ No PyJWT, no cryptography package.
10
10
  payload = auth.valid_token(token)
11
11
 
12
12
  # Legacy aliases also work:
13
- token = auth.create_token({"user_id": 1, "role": "admin"})
14
- payload = auth.validate_token(token)
13
+ token = auth.get_token({"user_id": 1, "role": "admin"})
14
+ payload = auth.valid_token(token)
15
15
 
16
16
  hashed = Auth.hash_password("secret123")
17
17
  Auth.check_password(hashed, "secret123") # True
@@ -29,27 +29,38 @@ class Auth:
29
29
  """JWT authentication and password hashing — zero dependencies."""
30
30
 
31
31
  def __init__(self, secret: str = None, algorithm: str = "HS256",
32
- token_expiry: int = None):
32
+ expires_in: int = None):
33
+ """
34
+ Args:
35
+ secret: Signing secret (falls back to SECRET env var).
36
+ algorithm: JWT algorithm (default HS256).
37
+ expires_in: Token lifetime in seconds (default 3600).
38
+ """
33
39
  self.secret = secret or os.environ.get("SECRET", "tina4-default-secret")
34
40
  self.algorithm = algorithm
35
- self.token_expiry = token_expiry or int(
36
- os.environ.get("TINA4_TOKEN_LIMIT", "30")
41
+ self.expires_in = expires_in or int(
42
+ os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60")
37
43
  )
38
44
 
39
45
  # ── JWT ────────────────────────────────────────────────────────
40
46
 
41
- def get_token(self, payload: dict, expiry_minutes: int = None) -> str:
47
+ def get_token(self, payload: dict, expires_in: int = None) -> str:
42
48
  """Create a signed JWT token.
43
49
 
50
+ Args:
51
+ expires_in: Lifetime in minutes (default: self.expires_in).
52
+
44
53
  Returns: header.payload.signature
45
54
  """
46
- exp = expiry_minutes if expiry_minutes is not None else self.token_expiry
55
+ exp_minutes = expires_in if expires_in is not None else self.expires_in
56
+ exp_seconds = exp_minutes * 60
57
+
47
58
  header = {"alg": self.algorithm, "typ": "JWT"}
48
59
 
49
60
  claims = dict(payload)
50
61
  claims["iat"] = int(time.time())
51
- if exp > 0:
52
- claims["exp"] = claims["iat"] + (exp * 60)
62
+ if exp_seconds > 0:
63
+ claims["exp"] = claims["iat"] + exp_seconds
53
64
 
54
65
  h = _b64url_encode(json.dumps(header).encode())
55
66
  p = _b64url_encode(json.dumps(claims, default=str).encode())
@@ -88,14 +99,18 @@ class Auth:
88
99
  except Exception:
89
100
  return None
90
101
 
91
- def refresh_token(self, token: str, expiry_minutes: int = None) -> str | None:
92
- """Validate and issue a fresh token with the same claims."""
102
+ def refresh_token(self, token: str, expires_in: int = None) -> str | None:
103
+ """Validate and issue a fresh token with the same claims.
104
+
105
+ Args:
106
+ expires_in: Lifetime in minutes (default: self.expires_in).
107
+ """
93
108
  payload = self.valid_token(token)
94
109
  if payload is None:
95
110
  return None
96
111
  payload.pop("iat", None)
97
112
  payload.pop("exp", None)
98
- return self.get_token(payload, expiry_minutes)
113
+ return self.get_token(payload, expires_in=expires_in)
99
114
 
100
115
  def _sign(self, message: str) -> str:
101
116
  sig = hmac.new(
@@ -105,8 +120,37 @@ class Auth:
105
120
 
106
121
  # ── Legacy aliases ─────────────────────────────────────────────
107
122
 
108
- create_token = get_token
109
- validate_token = valid_token
123
+ # get_token and valid_token are the primary names
124
+
125
+ # ── Class-level convenience methods (read SECRET from env) ────
126
+
127
+ @classmethod
128
+ def get_token_static(cls, payload: dict, expires_in: int = 60) -> str:
129
+ """Create a JWT without instantiating Auth — reads SECRET from env."""
130
+ secret = os.environ.get("SECRET", "tina4-default-secret")
131
+ auth = cls(secret=secret, expires_in=expires_in)
132
+ return auth.get_token(payload)
133
+
134
+ @classmethod
135
+ def valid_token_static(cls, token: str) -> dict | None:
136
+ """Validate a JWT without instantiating Auth — reads SECRET from env."""
137
+ secret = os.environ.get("SECRET", "tina4-default-secret")
138
+ auth = cls(secret=secret)
139
+ return auth.valid_token(token)
140
+
141
+ @classmethod
142
+ def get_payload_static(cls, token: str) -> dict | None:
143
+ """Decode payload (no validation) without instantiating Auth."""
144
+ secret = os.environ.get("SECRET", "tina4-default-secret")
145
+ auth = cls(secret=secret)
146
+ return auth.get_payload(token)
147
+
148
+ @classmethod
149
+ def refresh_token_static(cls, token: str, expires_in: int = 60) -> str | None:
150
+ """Refresh a JWT without instantiating Auth — reads SECRET from env."""
151
+ secret = os.environ.get("SECRET", "tina4-default-secret")
152
+ auth = cls(secret=secret, expires_in=expires_in)
153
+ return auth.refresh_token(token)
110
154
 
111
155
  # ── Password Hashing ──────────────────────────────────────────
112
156
 
@@ -189,4 +233,26 @@ def _b64url_decode(s: str) -> bytes:
189
233
  return base64.urlsafe_b64decode(s)
190
234
 
191
235
 
192
- __all__ = ["Auth"]
236
+ # ── Module-level convenience functions (use static methods) ────
237
+
238
+ def get_token(payload: dict, expires_in: int = 60) -> str:
239
+ """Create a JWT — reads SECRET from env. Shortcut for Auth.get_token_static()."""
240
+ return Auth.get_token_static(payload, expires_in=expires_in)
241
+
242
+
243
+ def valid_token(token: str) -> dict | None:
244
+ """Validate a JWT — reads SECRET from env. Shortcut for Auth.valid_token_static()."""
245
+ return Auth.valid_token_static(token)
246
+
247
+
248
+ def get_payload(token: str) -> dict | None:
249
+ """Decode JWT payload (no validation). Shortcut for Auth.get_payload_static()."""
250
+ return Auth.get_payload_static(token)
251
+
252
+
253
+ def refresh_token(token: str, expires_in: int = 60) -> str | None:
254
+ """Refresh a JWT — reads SECRET from env. Shortcut for Auth.refresh_token_static()."""
255
+ return Auth.refresh_token_static(token, expires_in=expires_in)
256
+
257
+
258
+ __all__ = ["Auth", "get_token", "valid_token", "get_payload", "refresh_token"]
@@ -75,6 +75,7 @@ class Request:
75
75
  return self.params.get(key, self._route_params.get(key, default))
76
76
 
77
77
 
78
+
78
79
  def _extract_ip(scope: dict, headers: dict) -> str:
79
80
  """Extract client IP, respecting X-Forwarded-For."""
80
81
  forwarded = headers.get("x-forwarded-for", "")
@@ -191,6 +191,10 @@ class Response:
191
191
 
192
192
  return self.html(f"<pre>Template not found: {template}</pre>", 404)
193
193
 
194
+ def template(self, template: str, data: dict = None) -> "Response":
195
+ """Alias for render() — parity with PHP/Node.js naming."""
196
+ return self.render(template, data)
197
+
194
198
  def build_headers(self, accept_encoding: str = "") -> list[tuple[bytes, bytes]]:
195
199
  """Build final ASGI headers with compression and ETag."""
196
200
  # Compress if applicable
@@ -39,6 +39,32 @@ class AutoCrud:
39
39
  # Track registered models for introspection
40
40
  _registered: dict[str, type] = {}
41
41
 
42
+ @staticmethod
43
+ def _build_example(model_class) -> dict:
44
+ """Build a sample request body from ORM field definitions.
45
+
46
+ Generates a dict with field names as keys and example values
47
+ based on field types, suitable for Swagger request body examples.
48
+ """
49
+ from datetime import datetime
50
+
51
+ example = {}
52
+ for name, field in model_class._fields.items():
53
+ if field.primary_key and field.auto_increment:
54
+ continue # Skip auto-generated PKs
55
+ ft = field.field_type
56
+ if ft == int:
57
+ example[name] = 0
58
+ elif ft == float:
59
+ example[name] = 0.0
60
+ elif ft == bool:
61
+ example[name] = True
62
+ elif ft == datetime:
63
+ example[name] = "2024-01-01T00:00:00"
64
+ else:
65
+ example[name] = "string"
66
+ return example
67
+
42
68
  @staticmethod
43
69
  def register(model_class, prefix: str = "/api"):
44
70
  """Register REST endpoints for a single ORM model class.
@@ -62,6 +88,8 @@ class AutoCrud:
62
88
  base_path = f"{prefix}/{table}"
63
89
  pk_field = model_class._get_pk()
64
90
  generated = []
91
+ pretty_name = table.replace("_", " ").title()
92
+ example_body = AutoCrud._build_example(model_class)
65
93
 
66
94
  # ── GET /api/{table} — list with pagination ──────────────
67
95
  async def list_handler(request, response, _cls=model_class):
@@ -82,6 +110,8 @@ class AutoCrud:
82
110
 
83
111
  list_handler.__name__ = f"autocrud_list_{table}"
84
112
  list_handler.__qualname__ = f"autocrud_list_{table}"
113
+ list_handler._swagger_summary = f"List all {pretty_name}"
114
+ list_handler._swagger_tags = [table]
85
115
  Router.add("GET", base_path, list_handler)
86
116
  generated.append({"method": "GET", "path": base_path, "table": table})
87
117
 
@@ -95,6 +125,8 @@ class AutoCrud:
95
125
 
96
126
  get_handler.__name__ = f"autocrud_get_{table}"
97
127
  get_handler.__qualname__ = f"autocrud_get_{table}"
128
+ get_handler._swagger_summary = f"Get {pretty_name} by ID"
129
+ get_handler._swagger_tags = [table]
98
130
  Router.add("GET", f"{base_path}/{{id}}", get_handler)
99
131
  generated.append({"method": "GET", "path": f"{base_path}/{{id}}", "table": table})
100
132
 
@@ -114,6 +146,9 @@ class AutoCrud:
114
146
  create_handler.__name__ = f"autocrud_create_{table}"
115
147
  create_handler.__qualname__ = f"autocrud_create_{table}"
116
148
  create_handler._noauth = True
149
+ create_handler._swagger_summary = f"Create {pretty_name}"
150
+ create_handler._swagger_tags = [table]
151
+ create_handler._swagger_example = example_body
117
152
  Router.add("POST", base_path, create_handler)
118
153
  generated.append({"method": "POST", "path": base_path, "table": table})
119
154
 
@@ -143,6 +178,9 @@ class AutoCrud:
143
178
  update_handler.__name__ = f"autocrud_update_{table}"
144
179
  update_handler.__qualname__ = f"autocrud_update_{table}"
145
180
  update_handler._noauth = True
181
+ update_handler._swagger_summary = f"Update {pretty_name}"
182
+ update_handler._swagger_tags = [table]
183
+ update_handler._swagger_example = example_body
146
184
  Router.add("PUT", f"{base_path}/{{id}}", update_handler)
147
185
  generated.append({"method": "PUT", "path": f"{base_path}/{{id}}", "table": table})
148
186
 
@@ -162,6 +200,8 @@ class AutoCrud:
162
200
  delete_handler.__name__ = f"autocrud_delete_{table}"
163
201
  delete_handler.__qualname__ = f"autocrud_delete_{table}"
164
202
  delete_handler._noauth = True
203
+ delete_handler._swagger_summary = f"Delete {pretty_name}"
204
+ delete_handler._swagger_tags = [table]
165
205
  Router.add("DELETE", f"{base_path}/{{id}}", delete_handler)
166
206
  generated.append({"method": "DELETE", "path": f"{base_path}/{{id}}", "table": table})
167
207
 
@@ -285,7 +285,7 @@ class DatabaseAdapter:
285
285
  )
286
286
 
287
287
  def fetch(self, sql: str, params: list = None,
288
- limit: int = 20, skip: int = 0) -> DatabaseResult:
288
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
289
289
  """Execute a read query and return multiple rows."""
290
290
  raise NotImplementedError
291
291
 
@@ -182,20 +182,21 @@ class Database:
182
182
  return self._adapter.execute_many(sql, params_list)
183
183
 
184
184
  def fetch(self, sql: str, params: list = None,
185
- limit: int = 20, skip: int = 0) -> DatabaseResult:
185
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
186
+ """Fetch rows with pagination."""
186
187
  if self._cache_enabled:
187
- key = self._cache_key(sql + f":L{limit}:S{skip}", params)
188
+ key = self._cache_key(sql + f":L{limit}:S{offset}", params)
188
189
  cached = self._cache_get(key)
189
190
  if cached is not None:
190
191
  with self._cache_lock:
191
192
  self._cache_hits += 1
192
193
  return cached
193
- result = self._adapter.fetch(sql, params, limit, skip)
194
+ result = self._adapter.fetch(sql, params, limit, offset)
194
195
  self._cache_set(key, result)
195
196
  with self._cache_lock:
196
197
  self._cache_misses += 1
197
198
  return result
198
- return self._adapter.fetch(sql, params, limit, skip)
199
+ return self._adapter.fetch(sql, params, limit, offset)
199
200
 
200
201
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
201
202
  if self._cache_enabled:
@@ -114,7 +114,7 @@ class FirebirdAdapter(DatabaseAdapter):
114
114
  )
115
115
 
116
116
  def fetch(self, sql: str, params: list = None,
117
- limit: int = 20, skip: int = 0) -> DatabaseResult:
117
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
118
118
  sql = self._translate_sql(sql)
119
119
  cursor = self._conn.cursor()
120
120
 
@@ -127,8 +127,8 @@ class FirebirdAdapter(DatabaseAdapter):
127
127
  total = 0
128
128
 
129
129
  # Apply Firebird pagination — ROWS start TO end
130
- start = skip + 1
131
- end = skip + limit
130
+ start = offset + 1
131
+ end = offset + limit
132
132
  paginated_sql = f"{sql} ROWS {start} TO {end}"
133
133
  cursor.execute(paginated_sql, params or [])
134
134
 
@@ -102,7 +102,7 @@ class MSSQLAdapter(DatabaseAdapter):
102
102
  )
103
103
 
104
104
  def fetch(self, sql: str, params: list = None,
105
- limit: int = 20, skip: int = 0) -> DatabaseResult:
105
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
106
106
  sql = self._translate_sql(sql)
107
107
  cursor = self._conn.cursor(as_dict=True)
108
108
 
@@ -121,7 +121,7 @@ class MSSQLAdapter(DatabaseAdapter):
121
121
  else:
122
122
  paginated_sql = f"{sql} OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
123
123
 
124
- paginated_params = tuple(params or []) + (skip, limit)
124
+ paginated_params = tuple(params or []) + (offset, limit)
125
125
  cursor.execute(paginated_sql, paginated_params)
126
126
  rows = [dict(row) for row in cursor.fetchall()]
127
127
 
@@ -91,7 +91,7 @@ class MySQLAdapter(DatabaseAdapter):
91
91
  )
92
92
 
93
93
  def fetch(self, sql: str, params: list = None,
94
- limit: int = 20, skip: int = 0) -> DatabaseResult:
94
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
95
95
  sql = self._translate_sql(sql)
96
96
  cursor = self._conn.cursor(dictionary=True)
97
97
 
@@ -105,7 +105,7 @@ class MySQLAdapter(DatabaseAdapter):
105
105
 
106
106
  # Apply pagination
107
107
  paginated_sql = f"{sql} LIMIT %s OFFSET %s"
108
- paginated_params = (params or []) + [limit, skip]
108
+ paginated_params = (params or []) + [limit, offset]
109
109
  cursor.execute(paginated_sql, paginated_params)
110
110
  rows = [dict(row) for row in cursor.fetchall()]
111
111
 
@@ -74,7 +74,7 @@ class ODBCAdapter(DatabaseAdapter):
74
74
  )
75
75
 
76
76
  def fetch(self, sql: str, params: list = None,
77
- limit: int = 20, skip: int = 0) -> DatabaseResult:
77
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
78
78
  # Count total
79
79
  count_sql = f"SELECT COUNT(*) FROM ({sql}) AS _t"
80
80
  cursor = self._conn.cursor()
@@ -86,14 +86,14 @@ class ODBCAdapter(DatabaseAdapter):
86
86
 
87
87
  # Apply pagination — use OFFSET/FETCH for ODBC (SQL Server style)
88
88
  paginated_sql = f"{sql} OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"
89
- paginated_params = (params or []) + [skip, limit]
89
+ paginated_params = (params or []) + [offset, limit]
90
90
 
91
91
  try:
92
92
  cursor.execute(paginated_sql, paginated_params)
93
93
  except Exception:
94
94
  # Fallback: try LIMIT/OFFSET for non-SQL Server ODBC sources
95
95
  paginated_sql = f"{sql} LIMIT ? OFFSET ?"
96
- paginated_params = (params or []) + [limit, skip]
96
+ paginated_params = (params or []) + [limit, offset]
97
97
  cursor.execute(paginated_sql, paginated_params)
98
98
 
99
99
  columns = [desc[0] for desc in cursor.description] if cursor.description else []
@@ -100,7 +100,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
100
100
  )
101
101
 
102
102
  def fetch(self, sql: str, params: list = None,
103
- limit: int = 20, skip: int = 0) -> DatabaseResult:
103
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
104
104
  import psycopg2.extras
105
105
 
106
106
  sql = self._translate_sql(sql)
@@ -117,7 +117,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
117
117
 
118
118
  # Apply pagination
119
119
  paginated_sql = f"{sql} LIMIT %s OFFSET %s"
120
- paginated_params = (params or []) + [limit, skip]
120
+ paginated_params = (params or []) + [limit, offset]
121
121
  cursor.execute(paginated_sql, paginated_params)
122
122
  rows = [dict(row) for row in cursor.fetchall()]
123
123
 
@@ -93,7 +93,7 @@ class SQLiteAdapter(DatabaseAdapter):
93
93
  )
94
94
 
95
95
  def fetch(self, sql: str, params: list = None,
96
- limit: int = 20, skip: int = 0) -> DatabaseResult:
96
+ limit: int = 20, offset: int = 0) -> DatabaseResult:
97
97
  # Count total rows (without LIMIT/OFFSET)
98
98
  count_sql = f"SELECT COUNT(*) as cnt FROM ({sql})"
99
99
  try:
@@ -103,7 +103,7 @@ class SQLiteAdapter(DatabaseAdapter):
103
103
 
104
104
  # Apply pagination
105
105
  paginated_sql = f"{sql} LIMIT ? OFFSET ?"
106
- paginated_params = (params or []) + [limit, skip]
106
+ paginated_params = (params or []) + [limit, offset]
107
107
  cursor = self._conn.execute(paginated_sql, paginated_params)
108
108
  rows = [dict(row) for row in cursor.fetchall()]
109
109
 
@@ -599,9 +599,9 @@ def _form_token(descriptor: str = "") -> str:
599
599
  payload["context"] = descriptor
600
600
 
601
601
  secret = os.environ.get("SECRET", "tina4-default-secret")
602
- ttl = int(os.environ.get("TINA4_TOKEN_LIMIT", "30"))
603
- auth = _FrondAuth(secret=secret, token_expiry=ttl)
604
- token = auth.create_token(payload)
602
+ ttl = int(os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60"))
603
+ auth = _FrondAuth(secret=secret, expires_in=ttl)
604
+ token = auth.get_token(payload)
605
605
  return SafeString(f'<input type="hidden" name="formToken" value="{token}">')
606
606
 
607
607
 
@@ -49,9 +49,9 @@ async def gallery_auth_page(request, response):
49
49
  <div class="card-body">
50
50
  <h6 style="color:#e2e8f0;">How it works</h6>
51
51
  <pre style="background:#0f172a;color:#4ade80;padding:1rem;border-radius:0.5rem;font-size:0.8rem;"><code>auth = Auth()
52
- token = auth.create_token({"username": "admin"})
52
+ token = auth.get_token({"username": "admin"})
53
53
  payload = auth.get_payload(token)
54
- is_valid = auth.validate_token(token)</code></pre>
54
+ is_valid = auth.valid_token(token)</code></pre>
55
55
  </div>
56
56
  </div>
57
57
  </div>
@@ -107,7 +107,7 @@ async def gallery_login(request, response):
107
107
  password = body.get("password", "")
108
108
  if username and password:
109
109
  auth = Auth()
110
- token = auth.create_token({"username": username, "role": "user"})
110
+ token = auth.get_token({"username": username, "role": "user"})
111
111
  return response({"token": token, "message": f"Welcome {username}!"})
112
112
  return response({"error": "Username and password required"}, 401)
113
113
 
@@ -117,5 +117,5 @@ async def gallery_login(request, response):
117
117
  async def gallery_verify(request, response):
118
118
  token = request.params.get("token", "")
119
119
  auth = Auth()
120
- is_valid = auth.validate_token(token)
120
+ is_valid = auth.valid_token(token)
121
121
  return response({"valid": is_valid})
@@ -24,7 +24,9 @@ class I18n:
24
24
  self._locale_dir = Path(
25
25
  locale_dir or os.environ.get("TINA4_LOCALE_DIR", "src/locales")
26
26
  )
27
- self._default_locale = default_locale or os.environ.get("TINA4_LANGUAGE", "en")
27
+ self._default_locale = default_locale or os.environ.get(
28
+ "TINA4_LOCALE", "en"
29
+ )
28
30
  self._current_locale = self._default_locale
29
31
  self._translations: dict[str, dict] = {}
30
32
  self._load_locale(self._default_locale)
@@ -232,7 +232,7 @@ class HasManyDescriptor(RelationshipDescriptor):
232
232
  table = related_cls._get_table()
233
233
  db = obj._get_db()
234
234
  sql = f"SELECT * FROM {table} WHERE {fk} = ?"
235
- result = db.fetch(sql, [pk_value], limit=1000, skip=0)
235
+ result = db.fetch(sql, [pk_value], limit=1000, offset=0)
236
236
  return [related_cls(row) for row in result.records]
237
237
 
238
238