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.
- {cloudoll-4.4.0 → cloudoll-4.5.0}/CHANGELOG.md +6 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/PKG-INFO +11 -8
- {cloudoll-4.4.0 → cloudoll-4.5.0}/README.md +8 -6
- {cloudoll-4.4.0 → cloudoll-4.5.0}/README.zh-CN.md +11 -9
- cloudoll-4.5.0/cloudoll/__init__.py +1 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/changelog.md +7 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/config.md +3 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/database.md +5 -3
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/deployment.md +14 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/start.md +3 -3
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/manifest.json +6 -6
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/m2d.py +25 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/schema.py +21 -5
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/__init__.py +8 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/aws_engine.py +3 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/base.py +1 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/compiler.py +27 -4
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/dialects.py +15 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/engine.py +9 -3
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/parse.py +13 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/protocols.py +3 -1
- cloudoll-4.5.0/cloudoll/orm/sqlite.py +223 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/README.md +1 -1
- cloudoll-4.5.0/cloudoll/template/requirements.txt +1 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/configuration.py +14 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/core.py +6 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/PKG-INFO +11 -8
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/SOURCES.txt +3 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/requires.txt +1 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/pyproject.toml +2 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_production_contracts.py +2 -0
- cloudoll-4.5.0/tests/test_sqlite_engine.py +70 -0
- cloudoll-4.5.0/tests/test_tls.py +21 -0
- cloudoll-4.4.0/cloudoll/__init__.py +0 -1
- cloudoll-4.4.0/cloudoll/template/requirements.txt +0 -1
- {cloudoll-4.4.0 → cloudoll-4.5.0}/AGENTS.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/AI.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/LICENSE +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/MANIFEST.in +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/SECURITY.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/__main__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/AGENTS.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/commands.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/ai.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/app-start.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/cookie-and-session.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/event-source.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/exception.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/file-uploads.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/http-client.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/jwt.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/logs.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/mail.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/middleware.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/migration.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/router.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/structure.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/utils.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/validation.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/view.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/docs/websockets.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/ai/skills/cloudoll/SKILL.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/cli.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/cli_main.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/process.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/spinner.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/clitool/watch.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/logging/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/mail/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/mail/smtp.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/observability.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/awsmysql.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/awspostgres.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/datasources.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/field.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/model.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/mysql.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/postgres.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/query.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/relation.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/savepoints.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/streaming.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/subquery.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/test_sql.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/orm/values.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/py.typed +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/.gitignore +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/config/conf.local.yaml +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/admin/test.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/api/index.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/es/es.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/home/index.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/upload/upload.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/controllers/ws/ws.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/middlewares/auth.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/css/index.css +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/img/cat.avif +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/static/js/index.js +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/template/templates/index.html +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/async_tools.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/common.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/password.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/utils/snowflake.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/__init__.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/context.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/exception.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/html.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/jwt.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/lifecycle.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/request_data.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/requests.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/resources.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/routing.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/sessions.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/settings.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/types.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll/web/validation.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/dependency_links.txt +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/entry_points.txt +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/cloudoll.egg-info/top_level.txt +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/architecture.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/aurora.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/maintenance.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/production.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/reliability.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/schema-generation.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/docs/streaming-and-types.md +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/scripts/sync_ai_docs.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/setup.cfg +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/setup.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/audit_dependencies.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/check_release.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/check_wheel.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/conftest.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_aws_failover.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_databases.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_savepoints.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_schema.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/integration/test_streaming.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/run_local_databases.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_ai.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_architecture.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_cli_create.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_datasources.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_dev_lifecycle.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_field_types.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_mail_client.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_password_and_id_utils.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_query_capabilities.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_regressions.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_release_workflow.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_request_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_retry_and_query_regressions.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_review_fixes.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_save_snapshots_and_cleanup.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_savepoint_unit.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_schema_generation.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_sql_echo.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_startup_logging.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_streaming_unit.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_transactions.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_typing_regressions.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/test_validation.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/engine_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/field_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/middleware_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/production_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/query_contract.py +0 -0
- {cloudoll-4.4.0 → cloudoll-4.5.0}/tests/typing/request_contract.py +0 -0
- {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
|
-
Summary:
|
|
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.
|
|
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.
|
|
75
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
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.
|
|
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.
|
|
177
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
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.
|
|
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.
|
|
23
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
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.
|
|
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.
|
|
125
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
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
|
-
|
|
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.
|
|
30
|
+
安装 4.5.0:
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
|
-
python -m pip install 'cloudoll==4.
|
|
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.
|
|
46
|
+
python -m pip install 'cloudoll[mysql]==4.5.0'
|
|
45
47
|
|
|
46
48
|
# PostgreSQL
|
|
47
|
-
python -m pip install 'cloudoll[postgres]==4.
|
|
49
|
+
python -m pip install 'cloudoll[postgres]==4.5.0'
|
|
48
50
|
|
|
49
51
|
# AWS 数据库驱动
|
|
50
|
-
python -m pip install 'cloudoll[aws]==4.
|
|
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.
|
|
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.
|
|
550
|
-
python tests/check_release.py dist/cloudoll-4.
|
|
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.
|
|
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/库名` 和 `
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
22
|
-
python -m pip install 'cloudoll[mysql,postgres,cache]==4.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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)
|