tina4-python 3.10.76__tar.gz → 3.10.83__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 (147) hide show
  1. {tina4_python-3.10.76 → tina4_python-3.10.83}/PKG-INFO +1 -1
  2. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/__init__.py +1 -1
  3. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/server.py +43 -10
  4. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/orm/__init__.py +1 -1
  5. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/orm/fields.py +1 -1
  6. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/orm/model.py +60 -6
  7. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/queue/__init__.py +212 -264
  8. tina4_python-3.10.83/tina4_python/queue/job.py +51 -0
  9. tina4_python-3.10.83/tina4_python/queue/lite_backend.py +300 -0
  10. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/websocket/__init__.py +62 -0
  11. {tina4_python-3.10.76 → tina4_python-3.10.83}/.gitignore +0 -0
  12. {tina4_python-3.10.76 → tina4_python-3.10.83}/README.md +0 -0
  13. {tina4_python-3.10.76 → tina4_python-3.10.83}/pyproject.toml +0 -0
  14. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/CLAUDE.md +0 -0
  15. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/HtmlElement.py +0 -0
  16. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/Testing.py +0 -0
  17. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/ai/__init__.py +0 -0
  18. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/api/__init__.py +0 -0
  19. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/auth/__init__.py +0 -0
  20. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/cache/__init__.py +0 -0
  21. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/cli/__init__.py +0 -0
  22. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/container/__init__.py +0 -0
  23. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/__init__.py +0 -0
  24. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/cache.py +0 -0
  25. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/constants.py +0 -0
  26. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/events.py +0 -0
  27. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/middleware.py +0 -0
  28. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/request.py +0 -0
  29. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/response.py +0 -0
  30. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/core/router.py +0 -0
  31. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/database/sqlite.py +0 -0
  42. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/debug/__init__.py +0 -0
  43. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/debug/error_overlay.py +0 -0
  44. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/dev_admin/__init__.py +0 -0
  45. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/dev_admin/metrics.py +0 -0
  46. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/dev_reload.py +0 -0
  47. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/frond/FROND.md +0 -0
  49. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/frond/__init__.py +0 -0
  50. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/frond/engine.py +0 -0
  51. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/auth/meta.json +0 -0
  52. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  53. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/database/meta.json +0 -0
  54. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  55. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/error-overlay/meta.json +0 -0
  56. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  57. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/orm/meta.json +0 -0
  58. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  59. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  60. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/queue/meta.json +0 -0
  61. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  62. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/rest-api/meta.json +0 -0
  63. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  64. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/templates/meta.json +0 -0
  65. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  66. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  67. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/graphql/__init__.py +0 -0
  68. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/i18n/__init__.py +0 -0
  69. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/mcp/__init__.py +0 -0
  70. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/mcp/protocol.py +0 -0
  71. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/mcp/tools.py +0 -0
  72. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/messenger/__init__.py +0 -0
  73. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/migration/__init__.py +0 -0
  74. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/migration/runner.py +0 -0
  75. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/css/tina4.css +0 -0
  76. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/css/tina4.min.css +0 -0
  77. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/favicon.ico +0 -0
  78. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/images/logo.svg +0 -0
  79. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  80. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/js/frond.min.js +0 -0
  81. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  82. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  83. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/js/tina4.min.js +0 -0
  84. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/js/tina4js.min.js +0 -0
  85. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/swagger/index.html +0 -0
  86. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  87. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/query_builder/__init__.py +0 -0
  88. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/queue_backends/__init__.py +0 -0
  89. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/queue_backends/kafka_backend.py +0 -0
  90. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/queue_backends/mongo_backend.py +0 -0
  91. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  92. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/__init__.py +0 -0
  93. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  94. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_badges.scss +0 -0
  95. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  96. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_cards.scss +0 -0
  97. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_forms.scss +0 -0
  98. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_grid.scss +0 -0
  99. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_modals.scss +0 -0
  100. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_nav.scss +0 -0
  101. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_reset.scss +0 -0
  102. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_tables.scss +0 -0
  103. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_typography.scss +0 -0
  104. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  105. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/_variables.scss +0 -0
  106. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/base.scss +0 -0
  107. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/colors.scss +0 -0
  108. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/scss/tina4css/tina4.scss +0 -0
  109. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/seeder/__init__.py +0 -0
  110. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/service/__init__.py +0 -0
  111. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/session/__init__.py +0 -0
  112. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/session_handlers/__init__.py +0 -0
  113. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  114. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/session_handlers/redis_handler.py +0 -0
  115. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/session_handlers/valkey_handler.py +0 -0
  116. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/swagger/__init__.py +0 -0
  117. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/components/crud.twig +0 -0
  118. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  119. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  120. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/docker/python/Dockerfile +0 -0
  121. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  122. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/302.twig +0 -0
  123. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/401.twig +0 -0
  124. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/403.twig +0 -0
  125. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/404.twig +0 -0
  126. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/500.twig +0 -0
  127. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/502.twig +0 -0
  128. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/503.twig +0 -0
  129. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/errors/base.twig +0 -0
  130. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/frontend/README.md +0 -0
  131. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/templates/readme.md +0 -0
  132. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/test_client/__init__.py +0 -0
  133. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  134. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  135. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/validator/__init__.py +0 -0
  146. {tina4_python-3.10.76 → tina4_python-3.10.83}/tina4_python/websocket/backplane.py +0 -0
  147. {tina4_python-3.10.76 → tina4_python-3.10.83}/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.10.76
3
+ Version: 3.10.83
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -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.10.76"
11
+ __version__ = "3.10.83"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -629,9 +629,24 @@ async def _handle_dev_websocket(reader, writer, headers, path):
629
629
 
630
630
 
631
631
  def _init_session(request: Request) -> None:
632
- """Auto-start session from cookie. Modifies request.session in place."""
632
+ """Auto-start session from cookie. Modifies request.session in place.
633
+
634
+ Session creation is skipped for WebSocket upgrade requests — they don't send
635
+ cookies and would create orphaned session files that are never cleaned up.
636
+ A session is only created when:
637
+ - A tina4_session cookie is already present (resume existing session), OR
638
+ - The request is a normal HTTP request (not a WebSocket upgrade)
639
+ """
633
640
  if request.session is not None:
634
641
  return
642
+
643
+ # Skip session creation for WebSocket upgrade requests.
644
+ # WebSocket clients don't send cookies so sess.start(None) would create a
645
+ # new orphaned file every connection that is never cleaned up.
646
+ upgrade = request.headers.get("upgrade", "").lower()
647
+ if upgrade == "websocket":
648
+ return
649
+
635
650
  try:
636
651
  from tina4_python.session import Session
637
652
  cookie_header = request.headers.get("cookie", "")
@@ -641,8 +656,19 @@ def _init_session(request: Request) -> None:
641
656
  if part.startswith("tina4_session="):
642
657
  sid_match = part.split("=", 1)[1]
643
658
  break
644
- sess = Session()
645
- sess.start(sid_match)
659
+
660
+ # Only create a new session for HTTP requests that don't already have one.
661
+ # This prevents a new empty session file being written on every anonymous request.
662
+ if sid_match is None:
663
+ # Lazy session: attach a Session object but don't persist until the route
664
+ # explicitly writes to it. The session file is only created on first save().
665
+ sess = Session()
666
+ sess.start(None)
667
+ sess._is_new = True # flag for _finalize_response to skip saving if unused
668
+ else:
669
+ sess = Session()
670
+ sess.start(sid_match)
671
+
646
672
  request.session = sess
647
673
  # Probabilistic garbage collection (1% of requests)
648
674
  import random
@@ -938,15 +964,22 @@ def _finalize_response(
938
964
  except Exception:
939
965
  pass
940
966
 
941
- # Session save + cookie
967
+ # Session save + cookie.
968
+ # Skip saving if this was a brand-new session that the route never wrote to —
969
+ # that prevents empty orphaned session files accumulating on disk.
942
970
  if request.session is not None:
943
971
  try:
944
- request.session.save()
945
- sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
946
- if sid:
947
- ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
948
- samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
949
- response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
972
+ is_new_and_empty = (
973
+ getattr(request.session, '_is_new', False)
974
+ and not request.session.all()
975
+ )
976
+ if not is_new_and_empty:
977
+ request.session.save()
978
+ sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
979
+ if sid:
980
+ ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
981
+ samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
982
+ response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
950
983
  import random
951
984
  if random.randint(1, 100) == 1:
952
985
  request.session.gc()
@@ -12,7 +12,7 @@ Active Record ORM with SQL-first paradigm.
12
12
 
13
13
  # SQL-first — you write the SQL, ORM maps results
14
14
  users = User.select("SELECT * FROM users WHERE active = ?", [1])
15
- user = User.find(1)
15
+ user = User.find_by_id(1)
16
16
  user.name = "Updated"
17
17
  user.save()
18
18
  """
@@ -262,7 +262,7 @@ class BelongsToDescriptor(RelationshipDescriptor):
262
262
  fk_value = getattr(obj, fk, None)
263
263
  if fk_value is None:
264
264
  return None
265
- return related_cls.find(fk_value)
265
+ return related_cls.find_by_id(fk_value)
266
266
 
267
267
 
268
268
  def has_many(model_name: str, foreign_key: str = None) -> HasManyDescriptor:
@@ -365,15 +365,69 @@ class ORM(metaclass=ORMMeta):
365
365
  return cls.select_one(sql, [pk_value], include=include)
366
366
 
367
367
  @classmethod
368
- def find(cls, pk_value, include: list[str] = None):
369
- """Alias for find_by_id()."""
370
- return cls.find_by_id(pk_value, include)
368
+ def find(cls, filter: dict = None, limit: int = 100, offset: int = 0, order_by: str = None, include: list[str] = None):
369
+ """Find records by filter dict.
371
370
 
372
- def load(self, sql: str, params: list = None, include: list[str] = None) -> bool:
373
- """Load a record into this instance via selectOne.
371
+ Usage:
372
+ User.find({"name": "Alice"}) → [User, ...]
373
+ User.find({"age": 18}, limit=10) → [User, ...]
374
+ User.find(order_by="name ASC") → [User, ...]
375
+ User.find() → all records
376
+
377
+ Args:
378
+ filter: Dict of {column: value} pairs (AND-ed together).
379
+ limit: Max records to return.
380
+ offset: Starting offset.
381
+ order_by: ORDER BY clause (e.g. "name ASC").
382
+ include: Relationship names to eager-load.
383
+ """
384
+ db = cls._get_db()
385
+ table = cls._get_table()
386
+ conditions = []
387
+ params = []
388
+
389
+ if filter:
390
+ for key, value in filter.items():
391
+ col = cls.field_mapping.get(key, key)
392
+ conditions.append(f"{col} = ?")
393
+ params.append(value)
394
+
395
+ if cls.soft_delete:
396
+ conditions.append("deleted_at IS NULL")
397
+
398
+ sql = f"SELECT * FROM {table}"
399
+ if conditions:
400
+ sql += " WHERE " + " AND ".join(conditions)
401
+ if order_by:
402
+ sql += f" ORDER BY {order_by}"
403
+
404
+ return cls.select(sql, params, limit=limit, offset=offset, include=include)
405
+
406
+ def load(self, filter: str = None, params: list = None, include: list[str] = None) -> bool:
407
+ """Load a record into this instance.
408
+
409
+ Usage:
410
+ orm.id = 1; orm.load() — uses PK already set
411
+ orm.load("id = ?", [1]) — filter with params
412
+ orm.load("id = 1") — filter string
374
413
 
375
414
  Returns True if a record was found and loaded, False otherwise.
376
415
  """
416
+ db = self._get_db()
417
+ table = self._get_table()
418
+
419
+ if filter is None:
420
+ # No args — use the primary key value already set
421
+ pk = self._get_pk()
422
+ pk_value = getattr(self, pk, None)
423
+ if pk_value is None:
424
+ return False
425
+ pk_col = self.field_mapping.get(pk, self._fields[pk].column)
426
+ sql = f"SELECT * FROM {table} WHERE {pk_col} = ?"
427
+ params = [pk_value]
428
+ else:
429
+ sql = f"SELECT * FROM {table} WHERE {filter}"
430
+
377
431
  cls = type(self)
378
432
  result = cls.select_one(sql, params, include=include)
379
433
  if result is None:
@@ -614,7 +668,7 @@ class ORM(metaclass=ORMMeta):
614
668
  fk_value = getattr(self, fk, None)
615
669
  if fk_value is None:
616
670
  return None
617
- return related_class.find(fk_value)
671
+ return related_class.find_by_id(fk_value)
618
672
 
619
673
  @classmethod
620
674
  def _eager_load(cls, instances: list, include: list[str]):