s3rds 0.3.2__tar.gz → 0.3.4__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.
- {s3rds-0.3.2 → s3rds-0.3.4}/PKG-INFO +28 -16
- {s3rds-0.3.2 → s3rds-0.3.4}/README.md +17 -13
- {s3rds-0.3.2 → s3rds-0.3.4}/pyproject.toml +16 -9
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/__init__.py +10 -3
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/psycopg2/__init__.py +5 -4
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/pymysql.py +4 -5
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/server/index.html +6 -6
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/server/lambda_function.py +24 -9
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/server/s3_sqlite.py +55 -4
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/sqlite/__init__.py +1 -1
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/sqlite/base.py +1 -1
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/PKG-INFO +28 -16
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/requires.txt +5 -2
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/psycopg2/errors.py +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/psycopg2/extensions.py +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/psycopg2/extras.py +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds/server/__init__.py +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/SOURCES.txt +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/dependency_links.txt +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/entry_points.txt +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/s3rds.egg-info/top_level.txt +0 -0
- {s3rds-0.3.2 → s3rds-0.3.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s3rds
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A truly serverless RDS on your own AWS account: Lambda URL endpoint, SQLite on S3, ≈ $0 when idle. Drivers named after the standard clients: s3rds.sqlite, s3rds.pymysql, s3rds.psycopg2; SQLAlchemy via sqlite+s3rds://, Django via ENGINE s3rds.sqlite.
|
|
5
5
|
Author: Frank Zhang
|
|
6
6
|
License: MIT
|
|
@@ -16,20 +16,28 @@ Classifier: Intended Audience :: Developers
|
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
23
|
Classifier: Framework :: Django
|
|
24
|
+
Classifier: Framework :: Django :: 4.2
|
|
25
|
+
Classifier: Framework :: Django :: 5.2
|
|
26
|
+
Classifier: Framework :: Django :: 6.1
|
|
21
27
|
Classifier: Topic :: Database
|
|
22
28
|
Classifier: Topic :: Database :: Front-Ends
|
|
23
29
|
Requires-Python: >=3.9
|
|
24
30
|
Description-Content-Type: text/markdown
|
|
25
|
-
Requires-Dist: sqlglot<31,>=30.24
|
|
26
31
|
Provides-Extra: mysql
|
|
32
|
+
Requires-Dist: sqlglot<31,>=30.24; extra == "mysql"
|
|
33
|
+
Provides-Extra: postgres
|
|
34
|
+
Requires-Dist: sqlglot<31,>=30.24; extra == "postgres"
|
|
27
35
|
Provides-Extra: server
|
|
28
36
|
Requires-Dist: sqlean.py==3.50.4.5; extra == "server"
|
|
29
37
|
Provides-Extra: sqlalchemy
|
|
30
38
|
Requires-Dist: SQLAlchemy<2.1,>=2.0; extra == "sqlalchemy"
|
|
31
39
|
Provides-Extra: django
|
|
32
|
-
Requires-Dist: Django<6.2,>=
|
|
40
|
+
Requires-Dist: Django<6.2,>=4.2; extra == "django"
|
|
33
41
|
|
|
34
42
|
# s3rds
|
|
35
43
|
|
|
@@ -59,11 +67,13 @@ while nobody uses it. Deploy it once with Terraform from the
|
|
|
59
67
|
**Python drivers for [s3rds](https://github.com/cloudformer/s3rds) — a truly serverless RDS:
|
|
60
68
|
a Lambda URL as your database endpoint, SQLite on S3, ≈ $0 when idle.**
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
70
|
+
No dependencies for the native paths; the translating drivers take an extra. Each driver is a submodule **named after
|
|
71
|
+
the standard client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
64
72
|
|
|
65
73
|
```bash
|
|
66
|
-
pip install s3rds
|
|
74
|
+
pip install s3rds # s3rds.sqlite, sqlite+s3rds:// for SQLAlchemy, ENGINE s3rds.sqlite for Django
|
|
75
|
+
pip install "s3rds[mysql]" # + s3rds.pymysql (sqlglot: MySQL -> SQLite)
|
|
76
|
+
pip install "s3rds[postgres]" # + s3rds.psycopg2 (sqlglot: PostgreSQL -> SQLite)
|
|
67
77
|
```
|
|
68
78
|
|
|
69
79
|
```python
|
|
@@ -88,13 +98,15 @@ conn.transaction([ # all-or-nothing, single S3 w
|
|
|
88
98
|
])
|
|
89
99
|
```
|
|
90
100
|
|
|
91
|
-
| Submodule | SQL dialect | Mirrors |
|
|
92
|
-
|
|
93
|
-
| `s3rds.sqlite` | SQLite (no translation) | pymysql-shaped API |
|
|
94
|
-
| `s3rds
|
|
95
|
-
| `s3rds.
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
101
|
+
| Submodule | Kind | SQL dialect | Mirrors | Install |
|
|
102
|
+
|---|---|---|---|---|
|
|
103
|
+
| `s3rds.sqlite` | native | SQLite (no translation) | pymysql-shaped API | `s3rds` |
|
|
104
|
+
| `sqlite+s3rds://` | native | SQLite, written by SQLAlchemy | SQLAlchemy 2.0 Core / ORM | `s3rds` (+ SQLAlchemy) |
|
|
105
|
+
| `ENGINE: "s3rds.sqlite"` | native | SQLite, written by Django | Django 4.2, 5.2, 6.1 | `s3rds` (+ Django) |
|
|
106
|
+
| `s3rds.pymysql` | translated | MySQL, translated on the client | pymysql | `"s3rds[mysql]"` |
|
|
107
|
+
| `s3rds.psycopg2` | translated | PostgreSQL, translated on the client | psycopg2 (`extras`, `errors`, `extensions`) | `"s3rds[postgres]"` |
|
|
108
|
+
|
|
109
|
+
Native is the most reliable: the SQL is SQLite's to begin with. For a new project, prefer SQLAlchemy or Django.
|
|
98
110
|
|
|
99
111
|
**SQLAlchemy** (Core and ORM): only the URL changes — SQLAlchemy's own SQLite dialect writes the SQL,
|
|
100
112
|
`s3rds.sqlite` replaces the standard library's `sqlite3` underneath.
|
|
@@ -144,11 +156,11 @@ What the report was verified with; the full report: [REPORT.md](https://github.c
|
|
|
144
156
|
|
|
145
157
|
| | Version | Fixed by |
|
|
146
158
|
|---|---|---|
|
|
147
|
-
| Python (client) | 3.9
|
|
159
|
+
| Python (client) | 3.9, 3.10, 3.11, 3.12, 3.13, each in CI | `requires-python = ">=3.9"` |
|
|
148
160
|
| Server runtime | AWS Lambda `python3.12` | `terraform/main.tf` |
|
|
149
161
|
| SQLite (server engine) | 3.50.4, shipped in the Lambda zip (the runtime's own is 3.40.0) | extra `pip install "s3rds[server]"` = `sqlean.py==3.50.4.5` |
|
|
150
|
-
| sqlglot (MySQL translation) | 30.24 |
|
|
162
|
+
| sqlglot (MySQL / PostgreSQL translation) | 30.24 | extras `"s3rds[mysql]"` / `"s3rds[postgres]"` = `sqlglot>=30.24,<31` |
|
|
151
163
|
| SQLAlchemy | 2.0, tested on 2.0.55 | extra `pip install "s3rds[sqlalchemy]"` = `SQLAlchemy>=2.0,<2.1` |
|
|
152
|
-
| Django |
|
|
164
|
+
| Django | 4.2 (Python 3.9, 3.10), 5.2 (3.11, 3.12), 6.1 (3.12, 3.13), each in CI | extra `pip install "s3rds[django]"` = `Django>=4.2,<6.2` |
|
|
153
165
|
| MySQL (reference) | 8.0.46 in Docker | `tests/compat/docker-compose.yml`: `mysql:8.0.46` |
|
|
154
166
|
|
|
@@ -26,11 +26,13 @@ while nobody uses it. Deploy it once with Terraform from the
|
|
|
26
26
|
**Python drivers for [s3rds](https://github.com/cloudformer/s3rds) — a truly serverless RDS:
|
|
27
27
|
a Lambda URL as your database endpoint, SQLite on S3, ≈ $0 when idle.**
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
29
|
+
No dependencies for the native paths; the translating drivers take an extra. Each driver is a submodule **named after
|
|
30
|
+
the standard client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
pip install s3rds
|
|
33
|
+
pip install s3rds # s3rds.sqlite, sqlite+s3rds:// for SQLAlchemy, ENGINE s3rds.sqlite for Django
|
|
34
|
+
pip install "s3rds[mysql]" # + s3rds.pymysql (sqlglot: MySQL -> SQLite)
|
|
35
|
+
pip install "s3rds[postgres]" # + s3rds.psycopg2 (sqlglot: PostgreSQL -> SQLite)
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
```python
|
|
@@ -55,13 +57,15 @@ conn.transaction([ # all-or-nothing, single S3 w
|
|
|
55
57
|
])
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
| Submodule | SQL dialect | Mirrors |
|
|
59
|
-
|
|
60
|
-
| `s3rds.sqlite` | SQLite (no translation) | pymysql-shaped API |
|
|
61
|
-
| `s3rds
|
|
62
|
-
| `s3rds.
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
60
|
+
| Submodule | Kind | SQL dialect | Mirrors | Install |
|
|
61
|
+
|---|---|---|---|---|
|
|
62
|
+
| `s3rds.sqlite` | native | SQLite (no translation) | pymysql-shaped API | `s3rds` |
|
|
63
|
+
| `sqlite+s3rds://` | native | SQLite, written by SQLAlchemy | SQLAlchemy 2.0 Core / ORM | `s3rds` (+ SQLAlchemy) |
|
|
64
|
+
| `ENGINE: "s3rds.sqlite"` | native | SQLite, written by Django | Django 4.2, 5.2, 6.1 | `s3rds` (+ Django) |
|
|
65
|
+
| `s3rds.pymysql` | translated | MySQL, translated on the client | pymysql | `"s3rds[mysql]"` |
|
|
66
|
+
| `s3rds.psycopg2` | translated | PostgreSQL, translated on the client | psycopg2 (`extras`, `errors`, `extensions`) | `"s3rds[postgres]"` |
|
|
67
|
+
|
|
68
|
+
Native is the most reliable: the SQL is SQLite's to begin with. For a new project, prefer SQLAlchemy or Django.
|
|
65
69
|
|
|
66
70
|
**SQLAlchemy** (Core and ORM): only the URL changes — SQLAlchemy's own SQLite dialect writes the SQL,
|
|
67
71
|
`s3rds.sqlite` replaces the standard library's `sqlite3` underneath.
|
|
@@ -111,11 +115,11 @@ What the report was verified with; the full report: [REPORT.md](https://github.c
|
|
|
111
115
|
|
|
112
116
|
| | Version | Fixed by |
|
|
113
117
|
|---|---|---|
|
|
114
|
-
| Python (client) | 3.9
|
|
118
|
+
| Python (client) | 3.9, 3.10, 3.11, 3.12, 3.13, each in CI | `requires-python = ">=3.9"` |
|
|
115
119
|
| Server runtime | AWS Lambda `python3.12` | `terraform/main.tf` |
|
|
116
120
|
| SQLite (server engine) | 3.50.4, shipped in the Lambda zip (the runtime's own is 3.40.0) | extra `pip install "s3rds[server]"` = `sqlean.py==3.50.4.5` |
|
|
117
|
-
| sqlglot (MySQL translation) | 30.24 |
|
|
121
|
+
| sqlglot (MySQL / PostgreSQL translation) | 30.24 | extras `"s3rds[mysql]"` / `"s3rds[postgres]"` = `sqlglot>=30.24,<31` |
|
|
118
122
|
| SQLAlchemy | 2.0, tested on 2.0.55 | extra `pip install "s3rds[sqlalchemy]"` = `SQLAlchemy>=2.0,<2.1` |
|
|
119
|
-
| Django |
|
|
123
|
+
| Django | 4.2 (Python 3.9, 3.10), 5.2 (3.11, 3.12), 6.1 (3.12, 3.13), each in CI | extra `pip install "s3rds[django]"` = `Django>=4.2,<6.2` |
|
|
120
124
|
| MySQL (reference) | 8.0.46 in Docker | `tests/compat/docker-compose.yml`: `mysql:8.0.46` |
|
|
121
125
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "s3rds"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "A truly serverless RDS on your own AWS account: Lambda URL endpoint, SQLite on S3, ≈ $0 when idle. Drivers named after the standard clients: s3rds.sqlite, s3rds.pymysql, s3rds.psycopg2; SQLAlchemy via sqlite+s3rds://, Django via ENGINE s3rds.sqlite."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -17,21 +17,28 @@ classifiers = [
|
|
|
17
17
|
"License :: OSI Approved :: MIT License",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
19
|
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
20
23
|
"Programming Language :: Python :: 3.13",
|
|
21
24
|
"Framework :: Django",
|
|
25
|
+
"Framework :: Django :: 4.2",
|
|
26
|
+
"Framework :: Django :: 5.2",
|
|
27
|
+
"Framework :: Django :: 6.1",
|
|
22
28
|
"Topic :: Database",
|
|
23
29
|
"Topic :: Database :: Front-Ends",
|
|
24
30
|
]
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# change between releases)
|
|
28
|
-
dependencies = [
|
|
31
|
+
# no dependencies: s3rds.sqlite (and SQLAlchemy / Django on it) is the standard library only. The translating drivers
|
|
32
|
+
# need sqlglot (MySQL / PostgreSQL -> SQLite), pinned to the series the compatibility report was verified with
|
|
33
|
+
# (translations change between releases): pip install "s3rds[mysql]" / "s3rds[postgres]".
|
|
34
|
+
dependencies = []
|
|
29
35
|
|
|
30
36
|
[project.optional-dependencies]
|
|
31
|
-
mysql = []
|
|
37
|
+
mysql = ["sqlglot>=30.24,<31"] # s3rds.pymysql
|
|
38
|
+
postgres = ["sqlglot>=30.24,<31"] # s3rds.psycopg2
|
|
32
39
|
server = ["sqlean.py==3.50.4.5"] # the Lambda: s3rds.server + SQLite 3.50.4 instead of the runtime's 3.40.0
|
|
33
40
|
sqlalchemy = ["SQLAlchemy>=2.0,<2.1"] # the series the compatibility report verified; pip install "s3rds[sqlalchemy]"
|
|
34
|
-
django = ["Django>=
|
|
41
|
+
django = ["Django>=4.2,<6.2"] # ENGINE "s3rds.sqlite"; CI runs 4.2 (Python 3.9, 3.10), 5.2 (3.11, 3.12), 6.1 (3.12, 3.13)
|
|
35
42
|
|
|
36
43
|
[project.entry-points."sqlalchemy.dialects"]
|
|
37
44
|
"sqlite.s3rds" = "s3rds.sqlite:SQLAlchemyDialect"
|
|
@@ -52,13 +59,13 @@ packages = ["s3rds", "s3rds.sqlite", "s3rds.psycopg2", "s3rds.server"]
|
|
|
52
59
|
|
|
53
60
|
# The versions tests/compat/REPORT.md was verified with; README.md, README.zh.md and client/README.md state the same.
|
|
54
61
|
[tool.s3rds.verified]
|
|
55
|
-
python = ["3.9.
|
|
62
|
+
python = ["3.9", "3.10", "3.11", "3.12", "3.13"] # the client: the full suite in CI (.github/workflows/test.yml)
|
|
56
63
|
lambda_runtime = "python3.12" # the server (terraform/main.tf)
|
|
57
64
|
sqlite = "3.50.4" # the server's engine, shipped in the Lambda zip (the runtime's own is 3.40.0)
|
|
58
65
|
sqlean = "3.50.4.5" # sqlean.py, which carries it (extra "server")
|
|
59
66
|
sqlglot = "30.24.0"
|
|
60
67
|
sqlalchemy = "2.0.55"
|
|
61
|
-
django = "
|
|
68
|
+
django = ["4.2", "5.2", "6.1"] # series; CI installs the latest release of each
|
|
62
69
|
mysql_reference = "8.0.46" # tests/compat/docker-compose.yml
|
|
63
70
|
postgres_reference = "16.4" # the same
|
|
64
71
|
psycopg2 = "2.9.12" # the reference client (psycopg2-binary)
|
|
@@ -14,9 +14,16 @@ uses SQLAlchemy's SQLite dialect with s3rds.sqlite as its driver; on RDS the URL
|
|
|
14
14
|
Django needs no import either: DATABASES ENGINE "s3rds.sqlite" (s3rds/sqlite/base.py) is Django's SQLite
|
|
15
15
|
backend on s3rds.sqlite; on RDS the ENGINE becomes django.db.backends.mysql.
|
|
16
16
|
"""
|
|
17
|
-
from . import sqlite # noqa: F401
|
|
18
|
-
from . import pymysql # noqa: F401 (sqlglot is only needed when a MySQL statement is actually translated)
|
|
19
|
-
from . import psycopg2 # noqa: F401
|
|
17
|
+
from . import sqlite # noqa: F401 (standard library only)
|
|
20
18
|
|
|
21
19
|
__all__ = ["sqlite", "pymysql", "psycopg2"]
|
|
22
20
|
__version__ = sqlite.__version__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __getattr__(name):
|
|
24
|
+
"""s3rds.pymysql / s3rds.psycopg2 load on first use: they translate with sqlglot (extras "mysql" / "postgres"),
|
|
25
|
+
which s3rds.sqlite, SQLAlchemy, Django and the server (s3rds.server) never need."""
|
|
26
|
+
if name in ("pymysql", "psycopg2"):
|
|
27
|
+
import importlib
|
|
28
|
+
return importlib.import_module(f".{name}", __name__)
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""s3rds.psycopg2 — the PostgreSQL-dialect driver for s3rds. Write PostgreSQL; it runs on the s3rds endpoint (SQLite engine).
|
|
2
2
|
|
|
3
|
+
pip install "s3rds[postgres]" # the extra brings sqlglot, the dialect translation
|
|
4
|
+
|
|
3
5
|
from s3rds import psycopg2 # later: `import psycopg2` on a real RDS, nothing else changes
|
|
4
6
|
from s3rds.psycopg2.extras import RealDictCursor
|
|
5
7
|
conn = psycopg2.connect(host="xxx.lambda-url.us-east-2.on.aws", dbname="blog", user="admin", password="...")
|
|
@@ -26,8 +28,9 @@ try:
|
|
|
26
28
|
import sqlglot
|
|
27
29
|
from sqlglot import exp
|
|
28
30
|
from sqlglot.errors import ParseError
|
|
29
|
-
except ImportError:
|
|
30
|
-
sqlglot
|
|
31
|
+
except ImportError:
|
|
32
|
+
raise ImportError('s3rds.psycopg2 translates PostgreSQL with sqlglot, which is not installed: '
|
|
33
|
+
'pip install "s3rds[postgres]"') from None
|
|
31
34
|
|
|
32
35
|
__version__ = _sqlite.__version__
|
|
33
36
|
apilevel = "2.0"
|
|
@@ -170,8 +173,6 @@ def _translate(sql):
|
|
|
170
173
|
tables = [t.strip().strip('"') for t in m.group(1).split(",")]
|
|
171
174
|
return tuple([f'DELETE FROM "{t}"' for t in tables]
|
|
172
175
|
+ ([f"DELETE FROM sqlite_sequence WHERE name = '{t}'" for t in tables] if m.group(2) else []))
|
|
173
|
-
if sqlglot is None:
|
|
174
|
-
raise NotSupportedError("the PostgreSQL dialect needs sqlglot (an s3rds dependency): pip install s3rds")
|
|
175
176
|
try:
|
|
176
177
|
tree = sqlglot.parse_one(sql, read="postgres")
|
|
177
178
|
except ParseError as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""s3rds.pymysql — the MySQL-dialect driver for s3rds. Write MySQL; it runs on the s3rds endpoint (SQLite engine).
|
|
2
2
|
|
|
3
|
-
pip install s3rds
|
|
3
|
+
pip install "s3rds[mysql]" # the extra brings sqlglot, the dialect translation
|
|
4
4
|
|
|
5
5
|
from s3rds import pymysql # later: `import pymysql` on a real RDS, nothing else changes
|
|
6
6
|
conn = pymysql.connect(host="xxx.lambda-url.us-east-2.on.aws",
|
|
@@ -42,8 +42,9 @@ try:
|
|
|
42
42
|
import sqlglot
|
|
43
43
|
from sqlglot import exp
|
|
44
44
|
from sqlglot.errors import ParseError
|
|
45
|
-
except ImportError:
|
|
46
|
-
sqlglot
|
|
45
|
+
except ImportError:
|
|
46
|
+
raise ImportError('s3rds.pymysql translates MySQL with sqlglot, which is not installed: '
|
|
47
|
+
'pip install "s3rds[mysql]"') from None
|
|
47
48
|
|
|
48
49
|
__version__ = _sqlite.__version__
|
|
49
50
|
apilevel = "2.0"
|
|
@@ -557,8 +558,6 @@ def _fix_delete_join(tree):
|
|
|
557
558
|
def _translate_stmts(sql):
|
|
558
559
|
"""MySQL statement -> tuple of SQLite statements (usually one; CREATE TABLE with inline indexes and
|
|
559
560
|
RENAME TABLE give several, a type-only ALTER gives none)."""
|
|
560
|
-
if sqlglot is None:
|
|
561
|
-
raise NotSupportedError(0, "the MySQL dialect needs sqlglot (an s3rds dependency): pip install s3rds")
|
|
562
561
|
m = _RENAME_TABLE.match(sql)
|
|
563
562
|
if m:
|
|
564
563
|
pairs = _RENAME_PAIR.findall(m.group(1))
|
|
@@ -191,9 +191,9 @@ dialog .form-grid { grid-template-columns: 1fr; }
|
|
|
191
191
|
|
|
192
192
|
<div class="controls">
|
|
193
193
|
<label>Database</label><select id="dbs"></select>
|
|
194
|
-
<button class="btn btn-secondary" id="newdb"
|
|
194
|
+
<button class="btn btn-secondary" id="newdb">+ New database</button>
|
|
195
195
|
<label>Table</label><select id="tables"></select>
|
|
196
|
-
<button class="btn btn-secondary" id="newtable"
|
|
196
|
+
<button class="btn btn-secondary" id="newtable">+ New table</button>
|
|
197
197
|
<button class="btn btn-ghost" id="refresh">refresh</button>
|
|
198
198
|
</div>
|
|
199
199
|
|
|
@@ -397,13 +397,13 @@ function render(){
|
|
|
397
397
|
$('#info-db').textContent = state.db || '-';
|
|
398
398
|
const main = $('#main');
|
|
399
399
|
if (!state.db){
|
|
400
|
-
main.innerHTML = '<div class="table-section"><div class="empty">Pick a database above, or click
|
|
400
|
+
main.innerHTML = '<div class="table-section"><div class="empty">Pick a database above, or click “+ New database”</div></div>';
|
|
401
401
|
return;
|
|
402
402
|
}
|
|
403
403
|
if (!state.table){
|
|
404
404
|
main.innerHTML = `<div class="table-section"><div class="empty">${state.tables.length
|
|
405
405
|
? 'Pick a table above'
|
|
406
|
-
: 'No tables yet — click
|
|
406
|
+
: 'No tables yet — click “+ New table”, or write CREATE TABLE in the SQL box'}</div></div>`;
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
409
|
if (!state.loaded){
|
|
@@ -461,7 +461,7 @@ function render(){
|
|
|
461
461
|
</div>
|
|
462
462
|
|
|
463
463
|
<div class="section-title">Structure
|
|
464
|
-
<button class="btn btn-secondary btn-small" id="addcol"
|
|
464
|
+
<button class="btn btn-secondary btn-small" id="addcol">+ Add column</button>
|
|
465
465
|
</div>
|
|
466
466
|
<div class="grid-wrap"><table>
|
|
467
467
|
<thead><tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th><th>actions</th></tr></thead>
|
|
@@ -594,7 +594,7 @@ $('#newtable').onclick = async () => {
|
|
|
594
594
|
try {
|
|
595
595
|
await api(`/api/db/${state.db}/query`, {method:'POST', body: JSON.stringify(
|
|
596
596
|
{sql: `CREATE TABLE "${name}" (id INTEGER PRIMARY KEY AUTOINCREMENT) STRICT`})});
|
|
597
|
-
msg(`✓ created table ${name} (STRICT, with an autoincrement id); add fields with
|
|
597
|
+
msg(`✓ created table ${name} (STRICT, with an autoincrement id); add fields with “+ Add column”`, true);
|
|
598
598
|
await loadTables(name);
|
|
599
599
|
await loadRows(); // open the table just created
|
|
600
600
|
} catch(e){ msg(friendly(e.message)); }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""s3rds server: a serverless database (SQL over HTTP, SQLite on S3).
|
|
2
2
|
|
|
3
|
-
GET / index.html (web console)
|
|
3
|
+
GET / index.html (web console; CONSOLE=0 -> 404, API only)
|
|
4
4
|
GET /api/databases list databases
|
|
5
5
|
POST /api/databases {name} create a database
|
|
6
6
|
GET /api/db/{db}/tables list tables
|
|
@@ -17,7 +17,9 @@ applied before BEGIN on every request (the server keeps no session).
|
|
|
17
17
|
Read / write is decided by the endpoint, never by guessing the SQL:
|
|
18
18
|
write endpoint (default) /query goes through run(): SQLite decides whether data changed, only then S3 is written
|
|
19
19
|
read-only endpoint READONLY=1 everything opens mode=ro; a write is refused by SQLite -> 403
|
|
20
|
-
Auth: HTTP Basic, the same meaning as RDS user / password.
|
|
20
|
+
Auth: HTTP Basic, the same meaning as RDS user / password. No DB_USER / DB_PASSWORD -> every request 503, unless
|
|
21
|
+
ALLOW_NO_AUTH=1 (local experiments only), which answers without auth and says so in x-s3rds-warning: no-auth.
|
|
22
|
+
ATTACH / DETACH / VACUUM INTO -> 403: a request works on its own database file only.
|
|
21
23
|
Every JSON reply carries ms (server time); data calls also carry s3 (cache-hit / download / +upload).
|
|
22
24
|
"""
|
|
23
25
|
import base64
|
|
@@ -27,14 +29,16 @@ import os
|
|
|
27
29
|
import re
|
|
28
30
|
import time
|
|
29
31
|
|
|
30
|
-
from .s3_sqlite import (SESSION_PRAGMAS, DatabaseExists, DatabaseNotFound, SqliteS3, WriteConflict,
|
|
32
|
+
from .s3_sqlite import (SESSION_PRAGMAS, DatabaseExists, DatabaseNotFound, NotAllowed, SqliteS3, WriteConflict,
|
|
31
33
|
create_database, list_databases, sqlite3)
|
|
32
34
|
|
|
33
35
|
BUCKET = os.environ["DB_BUCKET"]
|
|
34
36
|
PREFIX = os.environ.get("DB_PREFIX", "")
|
|
35
37
|
DB_USER = os.environ.get("DB_USER", "")
|
|
36
38
|
DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
|
|
39
|
+
ALLOW_NO_AUTH = os.environ.get("ALLOW_NO_AUTH") == "1" # no credentials configured: refuse (503) unless this is set
|
|
37
40
|
READONLY = os.environ.get("READONLY") == "1"
|
|
41
|
+
CONSOLE = os.environ.get("CONSOLE", "1") == "1" # the web console at GET /; unset = on
|
|
38
42
|
|
|
39
43
|
with open(os.path.join(os.path.dirname(__file__), "index.html"), encoding="utf-8") as f:
|
|
40
44
|
INDEX_HTML = f.read()
|
|
@@ -117,11 +121,15 @@ def _int(qs, key, default, lo, hi):
|
|
|
117
121
|
return max(lo, min(value, hi))
|
|
118
122
|
|
|
119
123
|
|
|
124
|
+
def _no_credentials():
|
|
125
|
+
return not DB_USER or not DB_PASSWORD
|
|
126
|
+
|
|
127
|
+
|
|
120
128
|
def _authed(event):
|
|
121
|
-
"""HTTP Basic, constant-time compare.
|
|
122
|
-
if
|
|
123
|
-
return
|
|
124
|
-
auth =
|
|
129
|
+
"""HTTP Basic, constant-time compare. The handler answers 503 before this when no credentials are configured."""
|
|
130
|
+
if _no_credentials():
|
|
131
|
+
return ALLOW_NO_AUTH
|
|
132
|
+
auth =(event.get("headers") or {}).get("authorization", "")
|
|
125
133
|
if not auth.lower().startswith("basic "):
|
|
126
134
|
return False
|
|
127
135
|
try:
|
|
@@ -379,12 +387,16 @@ def lambda_handler(event, context):
|
|
|
379
387
|
path = event.get("rawPath", "/")
|
|
380
388
|
|
|
381
389
|
if method == "GET" and path in ("/", "/index.html"):
|
|
390
|
+
if not CONSOLE:
|
|
391
|
+
return _json(404, {"error": "the web console is turned off (console = false); the API is under /api/"})
|
|
382
392
|
return {
|
|
383
393
|
"statusCode": 200,
|
|
384
394
|
"headers": {"content-type": "text/html; charset=utf-8"},
|
|
385
395
|
"body": INDEX_HTML,
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
if _no_credentials() and not ALLOW_NO_AUTH:
|
|
399
|
+
return _json(503, {"error": "server has no credentials configured (DB_USER / DB_PASSWORD)"})
|
|
388
400
|
if not _authed(event):
|
|
389
401
|
return _json(401, {"error": "bad credentials"})
|
|
390
402
|
|
|
@@ -399,7 +411,7 @@ def lambda_handler(event, context):
|
|
|
399
411
|
break
|
|
400
412
|
except BadRequest as e:
|
|
401
413
|
status, data = 400, {"error": str(e)}
|
|
402
|
-
except Forbidden as e:
|
|
414
|
+
except (Forbidden, NotAllowed) as e:
|
|
403
415
|
status, data = 403, {"error": str(e)}
|
|
404
416
|
except DatabaseNotFound as e:
|
|
405
417
|
status, data = 404, {"error": f"database not found: {e}"}
|
|
@@ -416,4 +428,7 @@ def lambda_handler(event, context):
|
|
|
416
428
|
status, data = 500, {"error": f"{type(e).__name__}: {e}"}
|
|
417
429
|
|
|
418
430
|
data.setdefault("ms", round((time.perf_counter() - t0) * 1000, 1))
|
|
419
|
-
|
|
431
|
+
resp = _json(status, data)
|
|
432
|
+
if _no_credentials(): # here only with ALLOW_NO_AUTH=1
|
|
433
|
+
resp["headers"]["x-s3rds-warning"] = "no-auth"
|
|
434
|
+
return resp
|
|
@@ -4,6 +4,7 @@ read(fn): opens the local cache mode=ro, SQLite itself refuses writes; never up
|
|
|
4
4
|
run(fn): after fn, SQLite's authorizer says whether data really changed; only then the whole file
|
|
5
5
|
goes back with If-Match (CAS); on 412 re-download, replay, retry; out of retries -> WriteConflict.
|
|
6
6
|
Rule: failing is allowed, duplicating a write or overwriting someone else's is not.
|
|
7
|
+
Every connection: ATTACH / DETACH (and so VACUUM INTO) refused -> NotAllowed; one request touches one file.
|
|
7
8
|
Cache: /tmp keyed by ETag; downloads go through a temp file + os.replace, never a half file.
|
|
8
9
|
One key = one database; the lock is the file, so different databases never conflict.
|
|
9
10
|
|
|
@@ -64,6 +65,45 @@ class WriteConflict(Exception):
|
|
|
64
65
|
pass
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
class NotAllowed(Exception):
|
|
69
|
+
"""A statement the authorizer refused: it would reach past this request's one database file."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ATTACH opens another file read/write even on a mode=ro connection: another database's /tmp cache (its path is
|
|
73
|
+
# sha1(bucket/key), so it can be guessed) could be changed and later uploaded. VACUUM INTO attaches too, so it is
|
|
74
|
+
# refused with it and cannot write files into /tmp either. Plain VACUUM attaches a temporary database as well and is
|
|
75
|
+
# refused the same way. On the read/write path SQLite stops any VACUUM before the authorizer is even asked (every
|
|
76
|
+
# request runs inside a transaction: "cannot VACUUM from within a transaction"); that error is reported as the same
|
|
77
|
+
# refusal, so both endpoints answer alike.
|
|
78
|
+
_DENIED_ACTIONS = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH}
|
|
79
|
+
_VACUUM_IN_TX = "cannot VACUUM from within a transaction" # SQLite's own message
|
|
80
|
+
NOT_ALLOWED = "ATTACH / DETACH / VACUUM INTO are not allowed: a request works on its own database only"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _Authorizer:
|
|
84
|
+
"""SQLite's authorizer for every connection: ATTACH / DETACH denied; on_allowed(action, arg1, arg2) sees the rest.
|
|
85
|
+
SQLite reports a denial as a plain DatabaseError ("not authorized"); denied says it was ours -> NotAllowed."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, on_allowed=None):
|
|
88
|
+
self.on_allowed = on_allowed
|
|
89
|
+
self.denied = False
|
|
90
|
+
|
|
91
|
+
def __call__(self, action, arg1, arg2, _dbname, _source): # signature fixed by sqlite3
|
|
92
|
+
if action in _DENIED_ACTIONS:
|
|
93
|
+
self.denied = True
|
|
94
|
+
return sqlite3.SQLITE_DENY
|
|
95
|
+
if self.on_allowed:
|
|
96
|
+
self.on_allowed(action, arg1, arg2)
|
|
97
|
+
return sqlite3.SQLITE_OK
|
|
98
|
+
|
|
99
|
+
def translate(self, e):
|
|
100
|
+
"""The exception to raise for e: NotAllowed when the authorizer refused (or SQLite refused a VACUUM inside
|
|
101
|
+
the request's transaction), else e itself."""
|
|
102
|
+
if isinstance(e, sqlite3.DatabaseError) and (self.denied or _VACUUM_IN_TX in str(e)):
|
|
103
|
+
return NotAllowed(NOT_ALLOWED)
|
|
104
|
+
return e
|
|
105
|
+
|
|
106
|
+
|
|
67
107
|
def _local_path(bucket, key):
|
|
68
108
|
digest = hashlib.sha1(f"{bucket}/{key}".encode()).hexdigest()
|
|
69
109
|
return f"/tmp/{digest}.sqlite"
|
|
@@ -208,9 +248,16 @@ class SqliteS3:
|
|
|
208
248
|
self._ensure_fresh()
|
|
209
249
|
conn = sqlite3.connect(f"file:{self.local}?mode=ro", uri=True, detect_types=detect_types)
|
|
210
250
|
conn.row_factory = sqlite3.Row
|
|
251
|
+
auth = _Authorizer()
|
|
211
252
|
try:
|
|
212
253
|
_apply_pragmas(conn, pragmas)
|
|
254
|
+
conn.set_authorizer(auth)
|
|
213
255
|
return fn(conn)
|
|
256
|
+
except sqlite3.DatabaseError as e:
|
|
257
|
+
err = auth.translate(e)
|
|
258
|
+
if err is e:
|
|
259
|
+
raise
|
|
260
|
+
raise err from None
|
|
214
261
|
finally:
|
|
215
262
|
conn.close()
|
|
216
263
|
|
|
@@ -276,7 +323,7 @@ class SqliteS3:
|
|
|
276
323
|
"""
|
|
277
324
|
dirty = False
|
|
278
325
|
|
|
279
|
-
def
|
|
326
|
+
def note_write(action, arg1, arg2):
|
|
280
327
|
nonlocal dirty
|
|
281
328
|
if action == sqlite3.SQLITE_PRAGMA:
|
|
282
329
|
# only PRAGMA x = v is a write; the file fingerprint still catches anything that changed the file
|
|
@@ -284,7 +331,8 @@ class SqliteS3:
|
|
|
284
331
|
dirty = True
|
|
285
332
|
elif action not in _READ_ACTIONS:
|
|
286
333
|
dirty = True
|
|
287
|
-
|
|
334
|
+
|
|
335
|
+
authorizer = _Authorizer(note_write)
|
|
288
336
|
|
|
289
337
|
# isolation_level=None: no implicit transactions from the sqlite3 module; we BEGIN/COMMIT ourselves
|
|
290
338
|
conn = sqlite3.connect(self.local, isolation_level=None, detect_types=detect_types)
|
|
@@ -303,13 +351,16 @@ class SqliteS3:
|
|
|
303
351
|
raise sqlite3.OperationalError("COMMIT / END inside a transaction trial; use the commit request")
|
|
304
352
|
if conn.in_transaction: # fn may have committed itself
|
|
305
353
|
conn.execute("COMMIT" if commit else "ROLLBACK")
|
|
306
|
-
except BaseException:
|
|
354
|
+
except BaseException as e:
|
|
307
355
|
try:
|
|
308
356
|
if conn.in_transaction:
|
|
309
357
|
conn.execute("ROLLBACK")
|
|
310
358
|
except sqlite3.Error:
|
|
311
359
|
self._invalidate() # even ROLLBACK failed: the local state cannot be trusted
|
|
312
|
-
|
|
360
|
+
err = authorizer.translate(e)
|
|
361
|
+
if err is e:
|
|
362
|
+
raise
|
|
363
|
+
raise err from None
|
|
313
364
|
finally:
|
|
314
365
|
conn.close()
|
|
315
366
|
return result, dirty
|
|
@@ -239,7 +239,7 @@ class DatabaseWrapper(base.DatabaseWrapper):
|
|
|
239
239
|
# no register_functions(conn): Python functions would have to run on the server (DatabaseOperations above)
|
|
240
240
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
241
241
|
conn.execute("PRAGMA legacy_alter_table = OFF")
|
|
242
|
-
for init_command in self
|
|
242
|
+
for init_command in getattr(self, "init_commands", ()): # OPTIONS init_command: Django 5.1+ only
|
|
243
243
|
if init_command := init_command.strip():
|
|
244
244
|
conn.execute(init_command)
|
|
245
245
|
return conn
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s3rds
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A truly serverless RDS on your own AWS account: Lambda URL endpoint, SQLite on S3, ≈ $0 when idle. Drivers named after the standard clients: s3rds.sqlite, s3rds.pymysql, s3rds.psycopg2; SQLAlchemy via sqlite+s3rds://, Django via ENGINE s3rds.sqlite.
|
|
5
5
|
Author: Frank Zhang
|
|
6
6
|
License: MIT
|
|
@@ -16,20 +16,28 @@ Classifier: Intended Audience :: Developers
|
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
23
|
Classifier: Framework :: Django
|
|
24
|
+
Classifier: Framework :: Django :: 4.2
|
|
25
|
+
Classifier: Framework :: Django :: 5.2
|
|
26
|
+
Classifier: Framework :: Django :: 6.1
|
|
21
27
|
Classifier: Topic :: Database
|
|
22
28
|
Classifier: Topic :: Database :: Front-Ends
|
|
23
29
|
Requires-Python: >=3.9
|
|
24
30
|
Description-Content-Type: text/markdown
|
|
25
|
-
Requires-Dist: sqlglot<31,>=30.24
|
|
26
31
|
Provides-Extra: mysql
|
|
32
|
+
Requires-Dist: sqlglot<31,>=30.24; extra == "mysql"
|
|
33
|
+
Provides-Extra: postgres
|
|
34
|
+
Requires-Dist: sqlglot<31,>=30.24; extra == "postgres"
|
|
27
35
|
Provides-Extra: server
|
|
28
36
|
Requires-Dist: sqlean.py==3.50.4.5; extra == "server"
|
|
29
37
|
Provides-Extra: sqlalchemy
|
|
30
38
|
Requires-Dist: SQLAlchemy<2.1,>=2.0; extra == "sqlalchemy"
|
|
31
39
|
Provides-Extra: django
|
|
32
|
-
Requires-Dist: Django<6.2,>=
|
|
40
|
+
Requires-Dist: Django<6.2,>=4.2; extra == "django"
|
|
33
41
|
|
|
34
42
|
# s3rds
|
|
35
43
|
|
|
@@ -59,11 +67,13 @@ while nobody uses it. Deploy it once with Terraform from the
|
|
|
59
67
|
**Python drivers for [s3rds](https://github.com/cloudformer/s3rds) — a truly serverless RDS:
|
|
60
68
|
a Lambda URL as your database endpoint, SQLite on S3, ≈ $0 when idle.**
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
70
|
+
No dependencies for the native paths; the translating drivers take an extra. Each driver is a submodule **named after
|
|
71
|
+
the standard client it mirrors**, so moving to a real database is a one-line import change and nothing else:
|
|
64
72
|
|
|
65
73
|
```bash
|
|
66
|
-
pip install s3rds
|
|
74
|
+
pip install s3rds # s3rds.sqlite, sqlite+s3rds:// for SQLAlchemy, ENGINE s3rds.sqlite for Django
|
|
75
|
+
pip install "s3rds[mysql]" # + s3rds.pymysql (sqlglot: MySQL -> SQLite)
|
|
76
|
+
pip install "s3rds[postgres]" # + s3rds.psycopg2 (sqlglot: PostgreSQL -> SQLite)
|
|
67
77
|
```
|
|
68
78
|
|
|
69
79
|
```python
|
|
@@ -88,13 +98,15 @@ conn.transaction([ # all-or-nothing, single S3 w
|
|
|
88
98
|
])
|
|
89
99
|
```
|
|
90
100
|
|
|
91
|
-
| Submodule | SQL dialect | Mirrors |
|
|
92
|
-
|
|
93
|
-
| `s3rds.sqlite` | SQLite (no translation) | pymysql-shaped API |
|
|
94
|
-
| `s3rds
|
|
95
|
-
| `s3rds.
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
101
|
+
| Submodule | Kind | SQL dialect | Mirrors | Install |
|
|
102
|
+
|---|---|---|---|---|
|
|
103
|
+
| `s3rds.sqlite` | native | SQLite (no translation) | pymysql-shaped API | `s3rds` |
|
|
104
|
+
| `sqlite+s3rds://` | native | SQLite, written by SQLAlchemy | SQLAlchemy 2.0 Core / ORM | `s3rds` (+ SQLAlchemy) |
|
|
105
|
+
| `ENGINE: "s3rds.sqlite"` | native | SQLite, written by Django | Django 4.2, 5.2, 6.1 | `s3rds` (+ Django) |
|
|
106
|
+
| `s3rds.pymysql` | translated | MySQL, translated on the client | pymysql | `"s3rds[mysql]"` |
|
|
107
|
+
| `s3rds.psycopg2` | translated | PostgreSQL, translated on the client | psycopg2 (`extras`, `errors`, `extensions`) | `"s3rds[postgres]"` |
|
|
108
|
+
|
|
109
|
+
Native is the most reliable: the SQL is SQLite's to begin with. For a new project, prefer SQLAlchemy or Django.
|
|
98
110
|
|
|
99
111
|
**SQLAlchemy** (Core and ORM): only the URL changes — SQLAlchemy's own SQLite dialect writes the SQL,
|
|
100
112
|
`s3rds.sqlite` replaces the standard library's `sqlite3` underneath.
|
|
@@ -144,11 +156,11 @@ What the report was verified with; the full report: [REPORT.md](https://github.c
|
|
|
144
156
|
|
|
145
157
|
| | Version | Fixed by |
|
|
146
158
|
|---|---|---|
|
|
147
|
-
| Python (client) | 3.9
|
|
159
|
+
| Python (client) | 3.9, 3.10, 3.11, 3.12, 3.13, each in CI | `requires-python = ">=3.9"` |
|
|
148
160
|
| Server runtime | AWS Lambda `python3.12` | `terraform/main.tf` |
|
|
149
161
|
| SQLite (server engine) | 3.50.4, shipped in the Lambda zip (the runtime's own is 3.40.0) | extra `pip install "s3rds[server]"` = `sqlean.py==3.50.4.5` |
|
|
150
|
-
| sqlglot (MySQL translation) | 30.24 |
|
|
162
|
+
| sqlglot (MySQL / PostgreSQL translation) | 30.24 | extras `"s3rds[mysql]"` / `"s3rds[postgres]"` = `sqlglot>=30.24,<31` |
|
|
151
163
|
| SQLAlchemy | 2.0, tested on 2.0.55 | extra `pip install "s3rds[sqlalchemy]"` = `SQLAlchemy>=2.0,<2.1` |
|
|
152
|
-
| Django |
|
|
164
|
+
| Django | 4.2 (Python 3.9, 3.10), 5.2 (3.11, 3.12), 6.1 (3.12, 3.13), each in CI | extra `pip install "s3rds[django]"` = `Django>=4.2,<6.2` |
|
|
153
165
|
| MySQL (reference) | 8.0.46 in Docker | `tests/compat/docker-compose.yml`: `mysql:8.0.46` |
|
|
154
166
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|