tina4-python 3.5.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.5.0 → tina4_python-3.6.0}/PKG-INFO +5 -5
  2. {tina4_python-3.5.0 → tina4_python-3.6.0}/README.md +4 -4
  3. {tina4_python-3.5.0 → tina4_python-3.6.0}/pyproject.toml +1 -1
  4. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/CLAUDE.md +6 -7
  5. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/auth/__init__.py +81 -15
  7. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/request.py +1 -0
  8. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/response.py +4 -0
  9. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/adapter.py +1 -1
  10. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/connection.py +5 -4
  11. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/firebird.py +3 -3
  12. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/mssql.py +2 -2
  13. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/mysql.py +2 -2
  14. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/odbc.py +3 -3
  15. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/postgres.py +2 -2
  16. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/sqlite.py +2 -2
  17. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/frond/engine.py +3 -3
  18. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +4 -4
  19. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/i18n/__init__.py +3 -1
  20. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/orm/fields.py +1 -1
  21. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/orm/model.py +17 -17
  22. {tina4_python-3.5.0 → tina4_python-3.6.0}/.gitignore +0 -0
  23. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/HtmlElement.py +0 -0
  24. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/Testing.py +0 -0
  25. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/ai/__init__.py +0 -0
  26. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/api/__init__.py +0 -0
  27. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/cache/__init__.py +0 -0
  28. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/cli/__init__.py +0 -0
  29. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/container/__init__.py +0 -0
  30. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/__init__.py +0 -0
  31. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/cache.py +0 -0
  32. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/constants.py +0 -0
  33. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/events.py +0 -0
  34. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/middleware.py +0 -0
  35. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/router.py +0 -0
  36. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/core/server.py +0 -0
  37. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/crud/__init__.py +0 -0
  38. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/database/__init__.py +0 -0
  39. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/dev_reload.py +0 -0
  43. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/frond/FROND.md +0 -0
  45. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/frond/__init__.py +0 -0
  46. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/meta.json +0 -0
  47. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/database/meta.json +0 -0
  48. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  49. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
  50. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  51. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/meta.json +0 -0
  52. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  53. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  54. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/meta.json +0 -0
  55. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  56. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/meta.json +0 -0
  57. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  58. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/meta.json +0 -0
  59. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  60. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  61. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/graphql/__init__.py +0 -0
  62. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/messenger/__init__.py +0 -0
  63. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/migration/__init__.py +0 -0
  64. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/migration/runner.py +0 -0
  65. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/orm/__init__.py +0 -0
  66. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.css +0 -0
  67. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.min.css +0 -0
  68. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/favicon.ico +0 -0
  69. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/images/logo.svg +0 -0
  70. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  71. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/js/frond.min.js +0 -0
  72. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  73. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4.min.js +0 -0
  74. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4js.min.js +0 -0
  75. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/swagger/index.html +0 -0
  76. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  77. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/queue/__init__.py +0 -0
  78. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/queue_backends/__init__.py +0 -0
  79. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
  80. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
  81. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  82. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/__init__.py +0 -0
  83. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  84. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
  85. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  86. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
  87. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
  88. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
  89. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
  90. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
  91. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
  92. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
  93. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
  94. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  95. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
  96. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/base.scss +0 -0
  97. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/colors.scss +0 -0
  98. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
  99. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/seeder/__init__.py +0 -0
  100. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/service/__init__.py +0 -0
  101. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/session/__init__.py +0 -0
  102. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/session_handlers/__init__.py +0 -0
  103. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  104. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/session_handlers/redis_handler.py +0 -0
  105. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
  106. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/swagger/__init__.py +0 -0
  107. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/components/crud.twig +0 -0
  108. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  109. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  110. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
  111. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  112. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/302.twig +0 -0
  113. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/401.twig +0 -0
  114. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/403.twig +0 -0
  115. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/404.twig +0 -0
  116. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/500.twig +0 -0
  117. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/502.twig +0 -0
  118. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/503.twig +0 -0
  119. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/errors/base.twig +0 -0
  120. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/frontend/README.md +0 -0
  121. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/templates/readme.md +0 -0
  122. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  123. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  124. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  125. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  126. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  127. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  128. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.5.0 → tina4_python-3.6.0}/tina4_python/websocket/__init__.py +0 -0
  135. {tina4_python-3.5.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.5.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.5.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.5.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
@@ -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
 
@@ -297,7 +297,7 @@ class ORM(metaclass=ORMMeta):
297
297
  return result
298
298
 
299
299
  @classmethod
300
- def all(cls, limit: int = 100, skip: int = 0, include: list[str] = None):
300
+ def all(cls, limit: int = 100, offset: int = 0, include: list[str] = None):
301
301
  """Fetch all records (respects soft delete).
302
302
 
303
303
  Args:
@@ -310,14 +310,14 @@ class ORM(metaclass=ORMMeta):
310
310
  if cls.soft_delete:
311
311
  sql += " WHERE deleted_at IS NULL"
312
312
 
313
- result = db.fetch(sql, limit=limit, skip=skip)
313
+ result = db.fetch(sql, limit=limit, offset=offset)
314
314
  instances = [cls(row) for row in result.records]
315
315
  if include:
316
316
  cls._eager_load(instances, include)
317
317
  return instances, result.count
318
318
 
319
319
  @classmethod
320
- def select(cls, sql: str, params: list = None, limit: int = 20, skip: int = 0,
320
+ def select(cls, sql: str, params: list = None, limit: int = 20, offset: int = 0,
321
321
  include: list[str] = None):
322
322
  """SQL-first query — you write the SQL, ORM maps results.
323
323
 
@@ -325,14 +325,14 @@ class ORM(metaclass=ORMMeta):
325
325
  include: List of relationship names to eager-load.
326
326
  """
327
327
  db = cls._get_db()
328
- result = db.fetch(sql, params, limit=limit, skip=skip)
328
+ result = db.fetch(sql, params, limit=limit, offset=offset)
329
329
  instances = [cls(row) for row in result.records]
330
330
  if include:
331
331
  cls._eager_load(instances, include)
332
332
  return instances, result.count
333
333
 
334
334
  @classmethod
335
- def where(cls, filter_sql: str, params: list = None, limit: int = 20, skip: int = 0,
335
+ def where(cls, filter_sql: str, params: list = None, limit: int = 20, offset: int = 0,
336
336
  include: list[str] = None):
337
337
  """Query with WHERE clause shorthand.
338
338
 
@@ -346,19 +346,19 @@ class ORM(metaclass=ORMMeta):
346
346
  if cls.soft_delete:
347
347
  sql = f"SELECT * FROM {table} WHERE ({filter_sql}) AND deleted_at IS NULL"
348
348
 
349
- result = db.fetch(sql, params, limit=limit, skip=skip)
349
+ result = db.fetch(sql, params, limit=limit, offset=offset)
350
350
  instances = [cls(row) for row in result.records]
351
351
  if include:
352
352
  cls._eager_load(instances, include)
353
353
  return instances, result.count
354
354
 
355
355
  @classmethod
356
- def with_trashed(cls, filter_sql: str = "1=1", params: list = None, limit: int = 20, skip: int = 0):
356
+ def with_trashed(cls, filter_sql: str = "1=1", params: list = None, limit: int = 20, offset: int = 0):
357
357
  """Query including soft-deleted records."""
358
358
  db = cls._get_db()
359
359
  table = cls._get_table()
360
360
  sql = f"SELECT * FROM {table} WHERE {filter_sql}"
361
- result = db.fetch(sql, params, limit=limit, skip=skip)
361
+ result = db.fetch(sql, params, limit=limit, offset=offset)
362
362
  return [cls(row) for row in result.records], result.count
363
363
 
364
364
  @classmethod
@@ -473,18 +473,18 @@ class ORM(metaclass=ORMMeta):
473
473
 
474
474
  @classmethod
475
475
  def cached(cls, sql: str, params: list = None, ttl: int = 60,
476
- limit: int = 20, skip: int = 0):
476
+ limit: int = 20, offset: int = 0):
477
477
  """SQL query with result caching.
478
478
 
479
479
  Usage:
480
480
  users, count = User.cached("SELECT * FROM users WHERE active = ?", [1], ttl=120)
481
481
  """
482
- cache_key = f"{cls.__name__}:{Cache.query_key(sql, params)}:{limit}:{skip}"
482
+ cache_key = f"{cls.__name__}:{Cache.query_key(sql, params)}:{limit}:{offset}"
483
483
  cached = _query_cache.get(cache_key)
484
484
  if cached is not None:
485
485
  return cached
486
486
 
487
- result = cls.select(sql, params, limit=limit, skip=skip)
487
+ result = cls.select(sql, params, limit=limit, offset=offset)
488
488
  _query_cache.set(cache_key, result, ttl=ttl, tags=[cls.__name__])
489
489
  return result
490
490
 
@@ -506,7 +506,7 @@ class ORM(metaclass=ORMMeta):
506
506
  row = self._get_db().fetch_one(sql, [pk_value])
507
507
  return related_class(row) if row else None
508
508
 
509
- def has_many(self, related_class, foreign_key: str = None, limit: int = 100, skip: int = 0):
509
+ def has_many(self, related_class, foreign_key: str = None, limit: int = 100, offset: int = 0):
510
510
  """Load multiple related records (imperative style)."""
511
511
  pk = self._get_pk()
512
512
  pk_value = getattr(self, pk)
@@ -514,7 +514,7 @@ class ORM(metaclass=ORMMeta):
514
514
  table = related_class._get_table()
515
515
 
516
516
  sql = f"SELECT * FROM {table} WHERE {fk} = ?"
517
- result = self._get_db().fetch(sql, [pk_value], limit=limit, skip=skip)
517
+ result = self._get_db().fetch(sql, [pk_value], limit=limit, offset=offset)
518
518
  return [related_class(row) for row in result.records]
519
519
 
520
520
  def belongs_to(self, related_class, foreign_key: str = None):
@@ -570,7 +570,7 @@ class ORM(metaclass=ORMMeta):
570
570
  table = related_cls._get_table()
571
571
  placeholders = ",".join("?" for _ in pk_values)
572
572
  sql = f"SELECT * FROM {table} WHERE {fk} IN ({placeholders})"
573
- result = db.fetch(sql, pk_values, limit=len(pk_values) * 1000, skip=0)
573
+ result = db.fetch(sql, pk_values, limit=len(pk_values) * 1000, offset=0)
574
574
  related_records = [related_cls(row) for row in result.records]
575
575
 
576
576
  # Eager load nested relationships on related records
@@ -607,7 +607,7 @@ class ORM(metaclass=ORMMeta):
607
607
  placeholders = ",".join("?" for _ in fk_values)
608
608
  pk_col = related_cls.field_mapping.get(related_pk, related_cls._fields[related_pk].column)
609
609
  sql = f"SELECT * FROM {table} WHERE {pk_col} IN ({placeholders})"
610
- result = db.fetch(sql, fk_values, limit=len(fk_values) * 10, skip=0)
610
+ result = db.fetch(sql, fk_values, limit=len(fk_values) * 10, offset=0)
611
611
  related_records = [related_cls(row) for row in result.records]
612
612
 
613
613
  if nested:
@@ -627,8 +627,8 @@ class ORM(metaclass=ORMMeta):
627
627
  User.scope("active", "active = ?", [1])
628
628
  users, count = User.active()
629
629
  """
630
- def scope_method(limit: int = 20, skip: int = 0):
631
- return cls.where(filter_sql, params, limit=limit, skip=skip)
630
+ def scope_method(limit: int = 20, offset: int = 0):
631
+ return cls.where(filter_sql, params, limit=limit, offset=offset)
632
632
 
633
633
  setattr(cls, name, staticmethod(scope_method))
634
634
 
File without changes