cloudoll 4.4.0__tar.gz → 4.5.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 (174) hide show
  1. {cloudoll-4.4.0 → cloudoll-4.5.0}/CHANGELOG.md +6 -0
  2. {cloudoll-4.4.0 → cloudoll-4.5.0}/PKG-INFO +11 -8
  3. {cloudoll-4.4.0 → cloudoll-4.5.0}/README.md +8 -6
  4. {cloudoll-4.4.0 → cloudoll-4.5.0}/README.zh-CN.md +11 -9
  5. cloudoll-4.5.0/cloudoll/__init__.py +1 -0
  6. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/changelog.md +7 -1
  7. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/config.md +3 -1
  8. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/database.md +5 -3
  9. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/deployment.md +14 -0
  10. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/start.md +3 -3
  11. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/manifest.json +6 -6
  12. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/m2d.py +25 -1
  13. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/schema.py +21 -5
  14. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/__init__.py +8 -0
  15. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/aws_engine.py +3 -1
  16. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/base.py +1 -1
  17. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/compiler.py +27 -4
  18. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/dialects.py +15 -0
  19. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/engine.py +9 -3
  20. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/parse.py +13 -1
  21. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/protocols.py +3 -1
  22. cloudoll-4.5.0/cloudoll/orm/sqlite.py +223 -0
  23. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/README.md +1 -1
  24. cloudoll-4.5.0/cloudoll/template/requirements.txt +1 -0
  25. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/configuration.py +14 -1
  26. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/core.py +6 -0
  27. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/PKG-INFO +11 -8
  28. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/SOURCES.txt +3 -0
  29. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/requires.txt +1 -0
  30. {cloudoll-4.4.0 → cloudoll-4.5.0}/pyproject.toml +2 -1
  31. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_production_contracts.py +2 -0
  32. cloudoll-4.5.0/tests/test_sqlite_engine.py +70 -0
  33. cloudoll-4.5.0/tests/test_tls.py +21 -0
  34. cloudoll-4.4.0/cloudoll/__init__.py +0 -1
  35. cloudoll-4.4.0/cloudoll/template/requirements.txt +0 -1
  36. {cloudoll-4.4.0 → cloudoll-4.5.0}/AGENTS.md +0 -0
  37. {cloudoll-4.4.0 → cloudoll-4.5.0}/AI.md +0 -0
  38. {cloudoll-4.4.0 → cloudoll-4.5.0}/LICENSE +0 -0
  39. {cloudoll-4.4.0 → cloudoll-4.5.0}/MANIFEST.in +0 -0
  40. {cloudoll-4.4.0 → cloudoll-4.5.0}/SECURITY.md +0 -0
  41. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/__main__.py +0 -0
  42. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/AGENTS.md +0 -0
  43. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/__init__.py +0 -0
  44. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/commands.py +0 -0
  45. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/ai.md +0 -0
  46. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/app-start.md +0 -0
  47. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/cookie-and-session.md +0 -0
  48. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/event-source.md +0 -0
  49. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/exception.md +0 -0
  50. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/file-uploads.md +0 -0
  51. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/http-client.md +0 -0
  52. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/jwt.md +0 -0
  53. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/logs.md +0 -0
  54. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/mail.md +0 -0
  55. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/middleware.md +0 -0
  56. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/migration.md +0 -0
  57. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/router.md +0 -0
  58. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/structure.md +0 -0
  59. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/utils.md +0 -0
  60. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/validation.md +0 -0
  61. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/view.md +0 -0
  62. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/websockets.md +0 -0
  63. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/skills/cloudoll/SKILL.md +0 -0
  64. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/cli.py +0 -0
  65. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/__init__.py +0 -0
  66. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/cli_main.py +0 -0
  67. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/process.py +0 -0
  68. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/spinner.py +0 -0
  69. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/watch.py +0 -0
  70. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/logging/__init__.py +0 -0
  71. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/mail/__init__.py +0 -0
  72. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/mail/smtp.py +0 -0
  73. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/observability.py +0 -0
  74. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/awsmysql.py +0 -0
  75. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/awspostgres.py +0 -0
  76. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/datasources.py +0 -0
  77. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/field.py +0 -0
  78. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/model.py +0 -0
  79. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/mysql.py +0 -0
  80. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/postgres.py +0 -0
  81. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/query.py +0 -0
  82. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/relation.py +0 -0
  83. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/savepoints.py +0 -0
  84. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/streaming.py +0 -0
  85. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/subquery.py +0 -0
  86. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/test_sql.py +0 -0
  87. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/values.py +0 -0
  88. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/py.typed +0 -0
  89. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/.gitignore +0 -0
  90. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/__init__.py +0 -0
  91. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/config/conf.local.yaml +0 -0
  92. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/admin/test.py +0 -0
  93. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/api/index.py +0 -0
  94. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/es/es.py +0 -0
  95. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/home/index.py +0 -0
  96. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/upload/upload.py +0 -0
  97. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/ws/ws.py +0 -0
  98. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/middlewares/auth.py +0 -0
  99. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/css/index.css +0 -0
  100. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/img/cat.avif +0 -0
  101. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/js/index.js +0 -0
  102. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/templates/index.html +0 -0
  103. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/__init__.py +0 -0
  104. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/async_tools.py +0 -0
  105. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/common.py +0 -0
  106. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/password.py +0 -0
  107. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/snowflake.py +0 -0
  108. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/__init__.py +0 -0
  109. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/context.py +0 -0
  110. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/exception.py +0 -0
  111. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/html.py +0 -0
  112. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/jwt.py +0 -0
  113. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/lifecycle.py +0 -0
  114. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/request_data.py +0 -0
  115. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/requests.py +0 -0
  116. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/resources.py +0 -0
  117. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/routing.py +0 -0
  118. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/sessions.py +0 -0
  119. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/settings.py +0 -0
  120. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/types.py +0 -0
  121. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/validation.py +0 -0
  122. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/dependency_links.txt +0 -0
  123. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/entry_points.txt +0 -0
  124. {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/top_level.txt +0 -0
  125. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/architecture.md +0 -0
  126. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/aurora.md +0 -0
  127. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/maintenance.md +0 -0
  128. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/production.md +0 -0
  129. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/reliability.md +0 -0
  130. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/schema-generation.md +0 -0
  131. {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/streaming-and-types.md +0 -0
  132. {cloudoll-4.4.0 → cloudoll-4.5.0}/scripts/sync_ai_docs.py +0 -0
  133. {cloudoll-4.4.0 → cloudoll-4.5.0}/setup.cfg +0 -0
  134. {cloudoll-4.4.0 → cloudoll-4.5.0}/setup.py +0 -0
  135. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/audit_dependencies.py +0 -0
  136. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/check_release.py +0 -0
  137. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/check_wheel.py +0 -0
  138. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/conftest.py +0 -0
  139. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_aws_failover.py +0 -0
  140. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_databases.py +0 -0
  141. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_savepoints.py +0 -0
  142. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_schema.py +0 -0
  143. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_streaming.py +0 -0
  144. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/run_local_databases.py +0 -0
  145. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_ai.py +0 -0
  146. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_architecture.py +0 -0
  147. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_cli_create.py +0 -0
  148. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_datasources.py +0 -0
  149. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_dev_lifecycle.py +0 -0
  150. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_field_types.py +0 -0
  151. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_mail_client.py +0 -0
  152. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_password_and_id_utils.py +0 -0
  153. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_query_capabilities.py +0 -0
  154. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_regressions.py +0 -0
  155. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_release_workflow.py +0 -0
  156. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_request_contract.py +0 -0
  157. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_retry_and_query_regressions.py +0 -0
  158. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_review_fixes.py +0 -0
  159. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_save_snapshots_and_cleanup.py +0 -0
  160. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_savepoint_unit.py +0 -0
  161. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_schema_generation.py +0 -0
  162. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_sql_echo.py +0 -0
  163. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_startup_logging.py +0 -0
  164. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_streaming_unit.py +0 -0
  165. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_transactions.py +0 -0
  166. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_typing_regressions.py +0 -0
  167. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_validation.py +0 -0
  168. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/engine_contract.py +0 -0
  169. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/field_contract.py +0 -0
  170. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/middleware_contract.py +0 -0
  171. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/production_contract.py +0 -0
  172. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/query_contract.py +0 -0
  173. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/request_contract.py +0 -0
  174. {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/web_contract.py +0 -0
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.5.0
4
+
5
+ - Add native HTTPS support to `Application.run()` using `server.ssl_certfile` and `server.ssl_keyfile`, or a supplied `server.ssl_context`.
6
+ - Add an async SQLite ORM engine with bounded pooling, transactions (including `IMMEDIATE` mode), streaming, URL parsing, SQLite SQL dialect, schema generation and model introspection.
7
+ - Add SQLite upsert support and include `aiosqlite` as a runtime dependency.
8
+
3
9
  简体中文见 [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md)。
4
10
 
5
11
  ## 4.4.0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudoll
3
- Version: 4.4.0
4
- Summary: Assist in quickly creating distributable microservices based on aiohttp, with a focus on simplicity and ease of use.
3
+ Version: 4.5.0
4
+ Summary: A distributable microservice framework with async ORM support for MySQL, PostgreSQL and SQLite.
5
5
  Author-email: Qiu <smallerqiu@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/smallerqiu/cloudoll-py
@@ -28,6 +28,7 @@ Requires-Dist: psutil
28
28
  Requires-Dist: tabulate
29
29
  Requires-Dist: concurrent_log_handler
30
30
  Requires-Dist: portalocker
31
+ Requires-Dist: aiosqlite<1,>=0.20
31
32
  Provides-Extra: cache
32
33
  Requires-Dist: redis[hiredis]; extra == "cache"
33
34
  Requires-Dist: aiomcache; extra == "cache"
@@ -54,8 +55,10 @@ Dynamic: license-file
54
55
 
55
56
  English | [简体中文](README.zh-CN.md)
56
57
 
57
- Cloudoll 4.4.0 is an aiohttp-based Python library for routing, middleware,
58
+ Cloudoll 4.5.0 is an aiohttp-based Python library for routing, middleware,
58
59
  templates, sessions, JWT, database access and application scaffolding.
60
+ The ORM supports SQLite out of the box and MySQL/PostgreSQL through optional drivers;
61
+ `Application.run()` can serve HTTPS directly from PEM certificates.
59
62
 
60
63
  ## AI-assisted development
61
64
 
@@ -71,8 +74,8 @@ does not have `cloudoll ai --help`, consult its matching README and source first
71
74
  Python 3.9+ is required. Install only the optional drivers you use:
72
75
 
73
76
  ```sh
74
- python -m pip install 'cloudoll==4.4.0'
75
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.4.0'
77
+ python -m pip install 'cloudoll==4.5.0'
78
+ python -m pip install 'cloudoll[mysql,postgres,cache]==4.5.0'
76
79
  ```
77
80
 
78
81
  The AWS extra requires Python 3.10+. Aurora remains untested; see
@@ -87,7 +90,7 @@ python -m pip install -r requirements.txt
87
90
  cloudoll start -n myapp
88
91
  ```
89
92
 
90
- The generated project targets 4.4.0 and receives random JWT and session secrets
93
+ The generated project targets 4.5.0 and receives random JWT and session secrets
91
94
  in `.env`. Controllers
92
95
  and authentication middleware are examples: replace demo credentials and supply
93
96
  your own authorization and upload policies before deployment.
@@ -173,8 +176,8 @@ python -m ruff check cloudoll tests
173
176
  python -m mypy
174
177
  python -m pytest -q -m 'not integration' -k 'not aws'
175
178
  python -m build
176
- python tests/check_wheel.py dist/cloudoll-4.4.0-py3-none-any.whl
177
- python tests/check_release.py dist/cloudoll-4.4.0-py3-none-any.whl
179
+ python tests/check_wheel.py dist/cloudoll-4.5.0-py3-none-any.whl
180
+ python tests/check_release.py dist/cloudoll-4.5.0-py3-none-any.whl
178
181
  ```
179
182
 
180
183
  Native integration tests require explicitly configured disposable databases.
@@ -2,8 +2,10 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- Cloudoll 4.4.0 is an aiohttp-based Python library for routing, middleware,
5
+ Cloudoll 4.5.0 is an aiohttp-based Python library for routing, middleware,
6
6
  templates, sessions, JWT, database access and application scaffolding.
7
+ The ORM supports SQLite out of the box and MySQL/PostgreSQL through optional drivers;
8
+ `Application.run()` can serve HTTPS directly from PEM certificates.
7
9
 
8
10
  ## AI-assisted development
9
11
 
@@ -19,8 +21,8 @@ does not have `cloudoll ai --help`, consult its matching README and source first
19
21
  Python 3.9+ is required. Install only the optional drivers you use:
20
22
 
21
23
  ```sh
22
- python -m pip install 'cloudoll==4.4.0'
23
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.4.0'
24
+ python -m pip install 'cloudoll==4.5.0'
25
+ python -m pip install 'cloudoll[mysql,postgres,cache]==4.5.0'
24
26
  ```
25
27
 
26
28
  The AWS extra requires Python 3.10+. Aurora remains untested; see
@@ -35,7 +37,7 @@ python -m pip install -r requirements.txt
35
37
  cloudoll start -n myapp
36
38
  ```
37
39
 
38
- The generated project targets 4.4.0 and receives random JWT and session secrets
40
+ The generated project targets 4.5.0 and receives random JWT and session secrets
39
41
  in `.env`. Controllers
40
42
  and authentication middleware are examples: replace demo credentials and supply
41
43
  your own authorization and upload policies before deployment.
@@ -121,8 +123,8 @@ python -m ruff check cloudoll tests
121
123
  python -m mypy
122
124
  python -m pytest -q -m 'not integration' -k 'not aws'
123
125
  python -m build
124
- python tests/check_wheel.py dist/cloudoll-4.4.0-py3-none-any.whl
125
- python tests/check_release.py dist/cloudoll-4.4.0-py3-none-any.whl
126
+ python tests/check_wheel.py dist/cloudoll-4.5.0-py3-none-any.whl
127
+ python tests/check_release.py dist/cloudoll-4.5.0-py3-none-any.whl
126
128
  ```
127
129
 
128
130
  Native integration tests require explicitly configured disposable databases.
@@ -6,7 +6,9 @@
6
6
 
7
7
  Cloudoll 是一个基于 aiohttp 的 Python Web 开发库,提供路由自动注册、中间件、模板渲染、会话管理、JWT、数据库访问及命令行脚手架,帮助快速构建 Web 应用和微服务。
8
8
 
9
- 本文适用于 Cloudoll 4.4.0。从 3.x 升级前请阅读下方迁移说明。
9
+ ORM 内置 SQLite 支持,MySQL/PostgreSQL 按需安装驱动;`Application.run()` 可直接加载 PEM 证书启用 HTTPS。
10
+
11
+ 本文适用于 Cloudoll 4.5.0。从 3.x 升级前请阅读下方迁移说明。
10
12
 
11
13
  ## AI 辅助开发
12
14
 
@@ -25,10 +27,10 @@ Cloudoll 是一个基于 aiohttp 的 Python Web 开发库,提供路由自动
25
27
 
26
28
  ## 安装
27
29
 
28
- 安装 4.4.0:
30
+ 安装 4.5.0:
29
31
 
30
32
  ```sh
31
- python -m pip install 'cloudoll==4.4.0'
33
+ python -m pip install 'cloudoll==4.5.0'
32
34
  ```
33
35
 
34
36
  在当前仓库目录安装源码:
@@ -41,16 +43,16 @@ python -m pip install -e .
41
43
 
42
44
  ```sh
43
45
  # MySQL
44
- python -m pip install 'cloudoll[mysql]==4.4.0'
46
+ python -m pip install 'cloudoll[mysql]==4.5.0'
45
47
 
46
48
  # PostgreSQL
47
- python -m pip install 'cloudoll[postgres]==4.4.0'
49
+ python -m pip install 'cloudoll[postgres]==4.5.0'
48
50
 
49
51
  # AWS 数据库驱动
50
- python -m pip install 'cloudoll[aws]==4.4.0'
52
+ python -m pip install 'cloudoll[aws]==4.5.0'
51
53
 
52
54
  # Redis / Memcached 会话或 Redis 数据库
53
- python -m pip install 'cloudoll[cache]==4.4.0'
55
+ python -m pip install 'cloudoll[cache]==4.5.0'
54
56
  ```
55
57
 
56
58
  源码开发时可用 `python -m pip install -e '.[mysql,postgres,cache,dev]'` 安装。
@@ -546,8 +548,8 @@ cloudoll restart -n myapp
546
548
  python -m pip install -e '.[mysql,postgres,cache,dev]'
547
549
  python -m pytest -q -m 'not integration' -k 'not aws'
548
550
  python -m build
549
- python tests/check_wheel.py dist/cloudoll-4.4.0-py3-none-any.whl
550
- python tests/check_release.py dist/cloudoll-4.4.0-py3-none-any.whl
551
+ python tests/check_wheel.py dist/cloudoll-4.5.0-py3-none-any.whl
552
+ python tests/check_release.py dist/cloudoll-4.5.0-py3-none-any.whl
551
553
  ```
552
554
 
553
555
  最后一条命令用于验证构建出的 wheel,版本号变化后请调整文件名。测试使用模拟数据库连接及本机 HTTP 服务;真实数据库连接和 AWS 故障切换需要单独的集成环境。
@@ -0,0 +1 @@
1
+ __version__ = "4.5.0"
@@ -6,10 +6,16 @@ icon: logs
6
6
 
7
7
  # 更新日志
8
8
 
9
- 按版本倒序记录 Cloudoll 的主要变化。当前使用方法请以本站 4.4.0 文档为准,历史功能不代表当前仍保留相同 API。
9
+ 按版本倒序记录 Cloudoll 的主要变化。当前使用方法请以本站 4.5.0 文档为准,历史功能不代表当前仍保留相同 API。
10
10
 
11
11
  历史版本日期为对应源码提交日期。各版本源码可通过表格中的链接查看。
12
12
 
13
+ ## 4.5.0
14
+
15
+ - `Application.run()` 支持通过证书路径或自定义 `ssl.SSLContext` 启用 HTTPS。
16
+ - ORM 新增基于 `aiosqlite` 的 SQLite 引擎、事务、流式查询、upsert 和建表/模型反向生成支持。
17
+ - SQLite 事务可选择 `DEFERRED`、`IMMEDIATE` 或 `EXCLUSIVE` 模式;SQLite 不支持行级 `FOR UPDATE`。
18
+
13
19
  ## 4.4.0
14
20
 
15
21
  缺陷修复版本,向后兼容;Python 3.9+ 仍然支持。
@@ -113,7 +113,9 @@ database:
113
113
  url: $POSTGRES_URL
114
114
  ```
115
115
 
116
- URL 格式分别是 `mysql://用户:密码@主机:3306/库名` 和 `postgres://用户:密码@主机:5432/库名`。账号、密码含特殊字符时需 URL 编码;分字段配置更容易避免编码错误。显式关键字参数优先于 URL 中的同名选项。
116
+ URL 格式分别是 `mysql://用户:密码@主机:3306/库名`、`postgres://用户:密码@主机:5432/库名` 和 `sqlite:///相对或绝对路径.db`。账号、密码含特殊字符时需 URL 编码;分字段配置更容易避免编码错误。显式关键字参数优先于 URL 中的同名选项。
117
+
118
+ SQLite 无需额外驱动;内存数据库可写为 `sqlite:///:memory:`。服务器可在 `server` 下配置 `ssl_certfile` 与 `ssl_keyfile` 启用 HTTPS,也可通过 Python API 传入自定义 `ssl_context`。
117
119
 
118
120
  在请求中通过 `request.app.db.mysql` / `request.app.db.postgres` 取连接。详细的超时、TLS、事务和 SQL 日志见[数据库](/database)及[日志](/logs)。
119
121
 
@@ -6,7 +6,7 @@ icon: database
6
6
 
7
7
  # 数据库与 ORM
8
8
 
9
- 本页介绍 Cloudoll 4.4.0 的原生 MySQL/PostgreSQL 接口,包括保存点、流式查询与 SQL 日志。从 3.x 升级请先阅读[升级说明](/migration)。
9
+ 本页介绍 Cloudoll 4.5.0 的原生 MySQL、PostgreSQL 与 SQLite 接口,包括保存点、流式查询与 SQL 日志。从 3.x 升级请先阅读[升级说明](/migration)。
10
10
 
11
11
  ## 应用代码默认使用实体
12
12
 
@@ -26,7 +26,7 @@ Python API 的准确拼写是 `order_by` 和大写 `As`,不是 `orderby` / `as
26
26
  ## 安装与连接
27
27
 
28
28
  ```sh
29
- python -m pip install 'cloudoll[mysql,postgres]==4.4.0'
29
+ python -m pip install 'cloudoll[mysql,postgres]==4.5.0'
30
30
  ```
31
31
 
32
32
  Web 应用在 [database 配置](/config)中声明连接,由框架建立、关闭,通过 `request.app.db.别名` 获取。不要每个请求创建一个池,也不要在请求结束时关闭共享池。
@@ -50,7 +50,9 @@ if __name__ == "__main__":
50
50
  asyncio.run(main())
51
51
  ```
52
52
 
53
- DATABASE_URL 为 mysql://... 或 postgres://...。`create_engine` 是异步函数;同一个引擎在所属事件循环中复用。关闭前先退出所有事务和流式查询上下文。
53
+ DATABASE_URL 可以是 `mysql://...`、`postgres://...` 或 `sqlite:///path/to/app.db`。SQLite 内存库使用 `sqlite:///:memory:`,引擎会将池大小限制为 1,确保连接间共享同一个内存数据库。`create_engine` 是异步函数;同一个引擎在所属事件循环中复用。关闭前先退出所有事务和流式查询上下文。
54
+
55
+ SQLite 基于 `aiosqlite`,支持 ORM 查询、绑定参数、批量写入、事务、嵌套保存点、流式读取和模型建表。`async with engine.transaction(mode="IMMEDIATE")` 可在并发写前取得 SQLite 写锁;mode 支持 `DEFERRED`、`IMMEDIATE`、`EXCLUSIVE`。SQLite 不支持行级 `FOR UPDATE`。其余高级 SQL 函数和 DDL 特性仍按 SQLite 自身能力限制。
54
56
 
55
57
  ## 模型定义
56
58
 
@@ -6,6 +6,20 @@ icon: deployment
6
6
 
7
7
  # 应用部署
8
8
 
9
+ ## 直接启用 HTTPS
10
+
11
+ Cloudoll 可直接使用 PEM 证书和私钥:
12
+
13
+ ```yaml
14
+ server:
15
+ host: 0.0.0.0
16
+ port: 443
17
+ ssl_certfile: /etc/ssl/cloudoll/fullchain.pem
18
+ ssl_keyfile: /etc/ssl/cloudoll/privkey.pem
19
+ ```
20
+
21
+ `Application.run()` 会创建 TLS server context 并加载证书。也可在 Python 中通过 `server.ssl_context` 传入自定义 `ssl.SSLContext`,但不能与证书路径同时设置。证书续期后需重启进程以加载新证书。反向代理部署仍可由代理终止 TLS。
22
+
9
23
  ## 生产启动与 CLI 边界
10
24
 
11
25
  在应用根目录创建虚拟环境、安装经过验证并锁定版本的 requirements.txt:
@@ -9,7 +9,7 @@ path: /
9
9
 
10
10
  Cloudoll 是基于 aiohttp 的异步 Python Web 库,提供路由、模板、Session、ORM 和 CLI。
11
11
 
12
- > 本文档按 Cloudoll 4.4.0 源码更新,实际发布状态以 PyPI 为准。从 3.x 升级前,请先阅读[升级说明](/migration),确认配置和 API 的兼容性变化。
12
+ > 本文档按 Cloudoll 4.5.0 源码更新,实际发布状态以 PyPI 为准。从 3.x 升级前,请先阅读[升级说明](/migration),确认配置和 API 的兼容性变化。
13
13
 
14
14
  ## 环境与安装
15
15
 
@@ -18,8 +18,8 @@ Cloudoll 是基于 aiohttp 的异步 Python Web 库,提供路由、模板、Se
18
18
  ```sh
19
19
  python3 -m venv .venv
20
20
  source .venv/bin/activate
21
- python -m pip install 'cloudoll==4.4.0'
22
- python -m pip install 'cloudoll[mysql,postgres,cache]==4.4.0'
21
+ python -m pip install 'cloudoll==4.5.0'
22
+ python -m pip install 'cloudoll[mysql,postgres,cache]==4.5.0'
23
23
  ```
24
24
 
25
25
  Windows PowerShell 激活命令为 `.venv\Scripts\Activate.ps1`。只使用 Web 功能时不需要安装数据库 extras。
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.4.0",
2
+ "version": "4.5.0",
3
3
  "documents": {
4
4
  "ai": {
5
5
  "source": "src/views/ai.md",
@@ -14,12 +14,12 @@
14
14
  "changelog": {
15
15
  "source": "src/views/changelog.md",
16
16
  "url": "https://cloudoll.chuchur.com/changelog",
17
- "sha256": "54ac62d5384e70db5911d882df93e5a3b8fef5d7983363cb342c4a0d905d2fc8"
17
+ "sha256": "848012a2052408b97d5c1f5e2c23fe3a109e5787f513ff9fe8593aa8df5a9cd0"
18
18
  },
19
19
  "config": {
20
20
  "source": "src/views/config.md",
21
21
  "url": "https://cloudoll.chuchur.com/config",
22
- "sha256": "2e4f7c9dfbca9e71e179512e67ae188f608fdf4b84bdcb64a424fccfeb7603d0"
22
+ "sha256": "4a3437732d80bf9df315d7e67b05fc7582857082fd9f4ba646cead3f66f1fdde"
23
23
  },
24
24
  "cookie-and-session": {
25
25
  "source": "src/views/cookie-and-session.md",
@@ -29,12 +29,12 @@
29
29
  "database": {
30
30
  "source": "src/views/database.md",
31
31
  "url": "https://cloudoll.chuchur.com/database",
32
- "sha256": "aed56825418dd4f2aa1f587cee45f0a429905aef03428b4f2e6b5900ee8e9f39"
32
+ "sha256": "1f48429ffa99df95d554cc70429bb30098e3bc4c6601e97d0ab0dbd0b5cffd76"
33
33
  },
34
34
  "deployment": {
35
35
  "source": "src/views/deployment.md",
36
36
  "url": "https://cloudoll.chuchur.com/deployment",
37
- "sha256": "1ba77f6764d38ec0c510f15432de9b33608b5225a89fd6320c96f449d36f0d82"
37
+ "sha256": "5e4774561bf1cd91229314bb33659c8c2f36d3e7ab6889133489f22476eeac0e"
38
38
  },
39
39
  "event-source": {
40
40
  "source": "src/views/event-source.md",
@@ -89,7 +89,7 @@
89
89
  "start": {
90
90
  "source": "src/views/start.md",
91
91
  "url": "https://cloudoll.chuchur.com/",
92
- "sha256": "d9c42da379ce7913f8c058f2fdede3251dd4ae61473fce1af1cb023abea04f27"
92
+ "sha256": "4541305a1d3414cf4f62aa09c874d47c78dbafe3d03262d6bf1bd693f904b329"
93
93
  },
94
94
  "structure": {
95
95
  "source": "src/views/structure.md",
@@ -133,6 +133,16 @@ async def create_model(pool: DatabaseEngine, table_name: str) -> str:
133
133
 
134
134
  async def get_table_cols(pool: DatabaseEngine, table_name: str) -> list[Any]:
135
135
  driver = driver_name(pool.driver)
136
+ if driver == "sqlite":
137
+ rows = await pool.all(
138
+ "PRAGMA table_info(" + dialect_for(driver).identifier(table_name) + ")", None
139
+ )
140
+ return [
141
+ {"field": row["name"], "default": row["dflt_value"],
142
+ "column_type": row["type"], "null": "NO" if row["notnull"] else "YES",
143
+ "Key": "PRI" if row["pk"] else "", "comment": None}
144
+ for row in rows
145
+ ]
136
146
  if driver == "mysql":
137
147
  return await pool.all(
138
148
  "SHOW FULL COLUMNS FROM " + dialect_for(driver).identifier(table_name), None
@@ -163,6 +173,10 @@ async def get_table_cols(pool: DatabaseEngine, table_name: str) -> list[Any]:
163
173
 
164
174
  async def get_all_tables(pool: DatabaseEngine) -> list[Any]:
165
175
  driver = driver_name(pool.driver)
176
+ if driver == "sqlite":
177
+ return await pool.all(
178
+ "SELECT name AS table_name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name", None
179
+ )
166
180
  if driver == "mysql":
167
181
  return await pool.all(
168
182
  "SELECT TABLE_NAME AS table_name FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME",
@@ -299,7 +313,17 @@ def _default(value: Any, kind: str, postgres: bool) -> Any:
299
313
 
300
314
 
301
315
  def get_col(field: dict[str, Any], driver: str = "mysql") -> dict[str, Any]:
302
- postgres = driver_name(driver) == "postgres"
316
+ normalized_driver = driver_name(driver)
317
+ if normalized_driver == "sqlite":
318
+ kind = field.get("column_type", "text").lower().split("(", 1)[0]
319
+ type_map = {"int": "integer", "bigint": "integer", "real": "float", "numeric": "numeric", "blob": "text"}
320
+ return dict(name=field["field"], column_type=type_map.get(kind, kind),
321
+ primary_key=field.get("Key") == "PRI", default=_default(field.get("default"), kind, False),
322
+ charset=None, max_length=None, scale_length=None,
323
+ auto_increment=bool(field.get("auto_increment", False)),
324
+ NOT_NULL=field.get("null") == "NO", update_generated=False,
325
+ unsigned=False, comment=None)
326
+ postgres = normalized_driver == "postgres"
303
327
  if not postgres:
304
328
  match = re.fullmatch(
305
329
  r"([a-z]+)(?:\((\d+)(?:,(\d+))?\))?( unsigned)?", field["Type"].lower()
@@ -42,11 +42,13 @@ TYPES = (
42
42
 
43
43
 
44
44
  def driver_name(driver: str) -> str:
45
+ if driver == "sqlite":
46
+ return driver
45
47
  if driver == "mysql":
46
48
  return driver
47
49
  if driver in {"postgres", "postgresql", "postgressql"}:
48
50
  return "postgres"
49
- raise ValueError("Schema generation supports native MySQL/PostgreSQL only")
51
+ raise ValueError("Schema generation supports native MySQL/PostgreSQL/SQLite only")
50
52
 
51
53
 
52
54
  def current_timestamp(value: Any) -> Optional[str]:
@@ -91,6 +93,7 @@ def compile_column(
91
93
  ) -> CompiledQuery:
92
94
  driver = driver_name(driver)
93
95
  pg = driver == "postgres"
96
+ sqlite = driver == "sqlite"
94
97
  dialect = dialect_for(driver)
95
98
  kind = field.column_type.lower()
96
99
  if kind not in TYPES:
@@ -105,6 +108,8 @@ def compile_column(
105
108
  "timestamp without time zone",
106
109
  }:
107
110
  raise ValueError(f"MySQL cannot represent {kind}")
111
+ if sqlite and (field.unsigned or field.charset or field.update_generated or field.comment is not None):
112
+ raise ValueError("SQLite schema generation does not support unsigned, charset, update-generated or comment metadata")
108
113
  mapped = (
109
114
  {
110
115
  "tinyint": "smallint",
@@ -121,6 +126,15 @@ def compile_column(
121
126
  if pg
122
127
  else {"integer": "int", "real": "float", "double precision": "double"}
123
128
  ).get(kind, kind)
129
+ if sqlite:
130
+ mapped = {
131
+ "tinyint": "INTEGER", "smallint": "INTEGER", "mediumint": "INTEGER",
132
+ "int": "INTEGER", "integer": "INTEGER", "bigint": "INTEGER",
133
+ "boolean": "INTEGER", "float": "REAL", "double": "REAL",
134
+ "double precision": "REAL", "decimal": "NUMERIC", "numeric": "NUMERIC",
135
+ "varchar": "TEXT", "char": "TEXT", "mediumtext": "TEXT", "longtext": "TEXT",
136
+ "datetime": "TEXT", "timestamp": "TEXT", "date": "TEXT", "json": "TEXT",
137
+ }.get(kind, kind.upper())
124
138
  size, scale = field.max_length, field.scale_length
125
139
  if size is not None:
126
140
  size = _integer(size, "max_length", 0 if kind in TEMPORAL else 1)
@@ -158,13 +172,15 @@ def compile_column(
158
172
  if field.auto_increment:
159
173
  if kind not in INTEGER or field.default is not None:
160
174
  raise ValueError("Identity requires an integer without a separate default")
161
- if not pg and not field.primary_key:
175
+ if not pg and not sqlite and not field.primary_key:
162
176
  raise ValueError(
163
177
  "MySQL AUTO_INCREMENT requires a primary key in generated schemas"
164
178
  )
165
- sql += " GENERATED BY DEFAULT AS IDENTITY" if pg else " AUTO_INCREMENT"
179
+ sql += " GENERATED BY DEFAULT AS IDENTITY" if pg else (" AUTOINCREMENT" if sqlite else " AUTO_INCREMENT")
166
180
  if field.primary_key:
167
181
  sql += " PRIMARY KEY"
182
+ if sqlite and field.auto_increment and field.primary_key:
183
+ sql = dialect.identifier(field.name) + " INTEGER PRIMARY KEY AUTOINCREMENT"
168
184
  if field.NOT_NULL:
169
185
  sql += " NOT NULL"
170
186
  params: list[Any] = []
@@ -186,7 +202,7 @@ def compile_column(
186
202
  expression = current_timestamp(field.default) if kind in TEMPORAL else None
187
203
  default_sql = expression or parameter(field.default)
188
204
  # MySQL text/JSON defaults require expression parentheses (8.0.13+).
189
- if not pg and kind in {"text", "mediumtext", "longtext", "json"}:
205
+ if not pg and not sqlite and kind in {"text", "mediumtext", "longtext", "json"}:
190
206
  default_sql = "(" + default_sql + ")"
191
207
  sql += " DEFAULT " + default_sql
192
208
  if field.update_generated:
@@ -195,7 +211,7 @@ def compile_column(
195
211
  sql += " ON UPDATE CURRENT_TIMESTAMP" + (
196
212
  f"({size})" if size is not None else ""
197
213
  )
198
- if field.comment is not None and not pg:
214
+ if field.comment is not None and not pg and not sqlite:
199
215
  sql += " COMMENT " + parameter(field.comment)
200
216
  return CompiledQuery(sql, params)
201
217
 
@@ -52,6 +52,10 @@ async def create_engine(**kw: Any) -> Any:
52
52
  from .postgres import Postgres
53
53
 
54
54
  return await Postgres().create_engine(**{**configs, **query})
55
+ elif driver == "sqlite":
56
+ from .sqlite import SQLite
57
+
58
+ return await SQLite().create_engine(**{**configs, **query})
55
59
  elif driver in ["redis", "rediss"]:
56
60
  from redis import asyncio as aioredis
57
61
 
@@ -77,6 +81,10 @@ def __getattr__(name: str) -> Any:
77
81
  from .postgres import Postgres
78
82
 
79
83
  return Postgres
84
+ if name == "SQLite":
85
+ from .sqlite import SQLite
86
+
87
+ return SQLite
80
88
  if name == "Query":
81
89
  from .query import Query
82
90
 
@@ -312,7 +312,9 @@ class AwsEngine(SavepointMixin, MeteBase):
312
312
  raise
313
313
 
314
314
  @asynccontextmanager
315
- async def transaction(self) -> AsyncIterator[AwsEngine]:
315
+ async def transaction(self, *, mode: Optional[str] = None) -> AsyncIterator[AwsEngine]:
316
+ if mode is not None:
317
+ raise ValueError("Transaction mode is only supported by SQLite")
316
318
  if self._state() is not None:
317
319
  async with self.savepoint():
318
320
  yield self
@@ -27,7 +27,7 @@ class QueryTypes(Enum):
27
27
  class MeteBase:
28
28
  driver: str
29
29
 
30
- def transaction(self) -> AbstractAsyncContextManager[Any]:
30
+ def transaction(self, *, mode: Optional[str] = None) -> AbstractAsyncContextManager[Any]:
31
31
  raise NotImplementedError("This engine does not implement transactions")
32
32
 
33
33
  async def __aenter__(self: E) -> E:
@@ -125,17 +125,28 @@ class SQLCompiler:
125
125
  if isinstance(amount, bool) or not isinstance(amount, int) or amount < 0:
126
126
  raise ValueError("Interval amount must be a non-negative integer")
127
127
  sign = ">=" if op.startswith("LASTED_") else "<"
128
+ if getattr(self.dialect, "is_sqlite", False):
129
+ modifiers = {
130
+ "DAY": "days", "MONTH": "months", "YEAR": "years",
131
+ "HOUR": "hours", "MINUTE": "minutes", "SECOND": "seconds",
132
+ }
133
+ return f"datetime({column}) {sign} datetime('now', '-{amount} {modifiers[unit]}')"
128
134
  return f"{column} {sign} CURRENT_TIMESTAMP - {self.dialect.interval(amount, unit)}"
129
135
  if op in {"IS_TODAY", "THIS_WEEK", "THIS_MONTH", "THIS_YEAR"}:
130
136
  if op == "IS_TODAY":
131
137
  return f"DATE({column}) = CURRENT_DATE"
132
138
  unit = op.split("_")[1].lower()
139
+ if getattr(self.dialect, "is_sqlite", False):
140
+ formats = {"week": "%Y-%W", "month": "%Y-%m", "year": "%Y"}
141
+ return f"strftime('{formats[unit]}', {column}) = strftime('{formats[unit]}', 'now')"
133
142
  if self.dialect.is_postgres:
134
143
  return f"DATE_TRUNC('{unit}', {column}) = DATE_TRUNC('{unit}', CURRENT_DATE)"
135
144
  patterns = {"week": "%x-%v", "month": "%Y-%m", "year": "%Y"}
136
145
  pattern = patterns[unit]
137
146
  return f"DATE_FORMAT({column}, '{pattern}') = DATE_FORMAT(CURRENT_DATE, '{pattern}')"
138
147
  if op in {"JSON_CONTAINS_ARRAY", "JSON_CONTAINS_OBJECT"}:
148
+ if getattr(self.dialect, "is_sqlite", False):
149
+ raise NotImplementedError("JSON containment expressions are not supported by the SQLite ORM")
139
150
  values = node.rpt if isinstance(node.rpt, (tuple, list)) else (node.rpt,)
140
151
  args = ",".join(self.expression(v, params) for v in values)
141
152
  kind = "array" if op.endswith("ARRAY") else "object"
@@ -149,6 +160,8 @@ class SQLCompiler:
149
160
  return f"STRING_AGG(CAST({column} AS TEXT), ',')"
150
161
  return f"GROUP_CONCAT({column})"
151
162
  if op == "DATE_FORMAT":
163
+ if getattr(self.dialect, "is_sqlite", False):
164
+ raise NotImplementedError("DATE_FORMAT uses MySQL format strings and is not supported by SQLite")
152
165
  if self.dialect.is_postgres:
153
166
  raise NotImplementedError(
154
167
  "DATE_FORMAT uses MySQL format strings; use PostgreSQL SQL explicitly"
@@ -244,6 +257,8 @@ class SQLCompiler:
244
257
  if state.offset is not None:
245
258
  sql += f" OFFSET {state.offset}"
246
259
  if state.lock:
260
+ if getattr(self.dialect, "is_sqlite", False):
261
+ raise ValueError("SQLite does not support row-level FOR UPDATE locks; use an IMMEDIATE transaction")
247
262
  if state.lock not in {
248
263
  "FOR UPDATE",
249
264
  "FOR UPDATE NOWAIT",
@@ -328,7 +343,7 @@ class SQLCompiler:
328
343
  validate(update_fields, "update_fields")
329
344
  if model.__primary_key__ in update_fields:
330
345
  raise ValueError("upsert cannot update the primary key")
331
- if not self.dialect.is_postgres and conflict_fields is not None:
346
+ if not self.dialect.is_postgres and not getattr(self.dialect, "is_sqlite", False) and conflict_fields is not None:
332
347
  raise ValueError(
333
348
  "MySQL cannot select conflict_fields; any unique key can conflict"
334
349
  )
@@ -348,13 +363,21 @@ class SQLCompiler:
348
363
  + ",".join(self.dialect.identifier(key) for key in target)
349
364
  + ") DO UPDATE SET "
350
365
  )
366
+ elif getattr(self.dialect, "is_sqlite", False):
367
+ target = conflict_fields or ([model.__primary_key__] if model.__primary_key__ else [])
368
+ validate(target, "conflict_fields")
369
+ if set(target) & set(update_fields):
370
+ raise ValueError("upsert cannot update conflict target fields")
371
+ compiled_sql = (
372
+ compiled.sql + " ON CONFLICT ("
373
+ + ",".join(self.dialect.identifier(key) for key in target)
374
+ + ") DO UPDATE SET "
375
+ )
351
376
  else:
352
377
  compiled_sql = compiled.sql + " ON DUPLICATE KEY UPDATE "
353
378
  incoming = dict(zip(keys, values))
354
379
  # Bind incoming values again: avoids MySQL's deprecated VALUES(column).
355
- compiled_sql += ",".join(
356
- self.dialect.identifier(key) + "=?" for key in update_fields
357
- )
380
+ compiled_sql += ",".join(self.dialect.identifier(key) + "=?" for key in update_fields)
358
381
  return CompiledQuery(
359
382
  compiled_sql, compiled.params + [incoming[key] for key in update_fields]
360
383
  )
@@ -80,7 +80,22 @@ class PostgreSQLDialect(MySQLDialect):
80
80
  return f"INTERVAL '{amount} {unit.lower()}'"
81
81
 
82
82
 
83
+ class SQLiteDialect(MySQLDialect):
84
+ is_postgres = False
85
+ is_sqlite = True
86
+ quote = '"'
87
+
88
+ def _code(self, text: str) -> str:
89
+ text = re.sub(r"\bCURDATE\(\)", "date('now')", text, flags=re.I)
90
+ return re.sub(r"\bNOW\(\)", "datetime('now')", text, flags=re.I)
91
+
92
+ def prepare(self, sql: str, *, escape_percent: bool = False) -> str:
93
+ return self._code(sql)
94
+
95
+
83
96
  def dialect_for(driver: str) -> MySQLDialect:
97
+ if driver == "sqlite":
98
+ return SQLiteDialect()
84
99
  if driver in {
85
100
  "postgres",
86
101
  "postgresql",
@@ -239,13 +239,19 @@ class AsyncEngine(SavepointMixin, MeteBase):
239
239
  raise
240
240
 
241
241
  @asynccontextmanager
242
- async def transaction(self: AE) -> AsyncIterator[AE]:
242
+ async def transaction(self: AE, *, mode: Optional[str] = None) -> AsyncIterator[AE]:
243
243
  """Pin a connection to the current task; commit or roll back on exit.
244
244
 
245
245
  Nested scopes use savepoints. DDL/explicit transaction-control SQL is
246
246
  unsupported. Use one transaction per task; no automatic write retries.
247
247
  """
248
+ if mode is not None and getattr(self, "driver", None) != "sqlite":
249
+ raise ValueError("Transaction mode is only supported by SQLite")
250
+ if mode is not None and mode not in {"DEFERRED", "IMMEDIATE", "EXCLUSIVE"}:
251
+ raise ValueError("SQLite transaction mode must be DEFERRED, IMMEDIATE or EXCLUSIVE")
248
252
  if self._state() is not None:
253
+ if mode is not None:
254
+ raise TransactionError("Cannot change the mode of a nested transaction")
249
255
  async with self.savepoint():
250
256
  yield self
251
257
  return
@@ -262,7 +268,7 @@ class AsyncEngine(SavepointMixin, MeteBase):
262
268
  try:
263
269
  try:
264
270
  await asyncio.wait_for(
265
- self._control(connection, "BEGIN"), self.query_timeout
271
+ self._control(connection, "BEGIN" + (f" {mode}" if mode else "")), self.query_timeout
266
272
  )
267
273
  except (asyncio.CancelledError, asyncio.TimeoutError):
268
274
  connection.close()
@@ -396,7 +402,7 @@ class AsyncEngine(SavepointMixin, MeteBase):
396
402
  rows = None
397
403
  try:
398
404
  await asyncio.wait_for(
399
- self._control(connection, "START TRANSACTION READ ONLY"),
405
+ self._control(connection, "BEGIN" if self.driver == "sqlite" else "START TRANSACTION READ ONLY"),
400
406
  self.query_timeout,
401
407
  )
402
408
  self._log_sql("STREAM", sql, params)