pyflashdb 1.0.4__tar.gz → 2.0.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.
- pyflashdb-2.0.0/Changelog.md +135 -0
- {pyflashdb-1.0.4 → pyflashdb-2.0.0}/LICENSE +20 -20
- pyflashdb-2.0.0/MANIFEST.in +7 -0
- pyflashdb-2.0.0/PKG-INFO +440 -0
- pyflashdb-2.0.0/README.md +393 -0
- pyflashdb-2.0.0/docker-compose.yml +52 -0
- pyflashdb-2.0.0/examples/quickstart.py +80 -0
- pyflashdb-2.0.0/flash/__init__.py +37 -0
- pyflashdb-2.0.0/pyflashdb/__init__.py +44 -0
- pyflashdb-2.0.0/pyflashdb/_like.py +83 -0
- pyflashdb-2.0.0/pyflashdb/_util.py +58 -0
- pyflashdb-2.0.0/pyflashdb/adapters/__init__.py +6 -0
- pyflashdb-2.0.0/pyflashdb/adapters/base.py +141 -0
- pyflashdb-2.0.0/pyflashdb/adapters/mongo_adapter.py +718 -0
- pyflashdb-2.0.0/pyflashdb/adapters/mysql_adapter.py +242 -0
- pyflashdb-2.0.0/pyflashdb/adapters/postgres_adapter.py +243 -0
- pyflashdb-2.0.0/pyflashdb/adapters/sql.py +597 -0
- pyflashdb-2.0.0/pyflashdb/adapters/sqlite_adapter.py +287 -0
- pyflashdb-2.0.0/pyflashdb/core.py +762 -0
- pyflashdb-2.0.0/pyflashdb/dialects.py +345 -0
- pyflashdb-2.0.0/pyflashdb/exceptions.py +43 -0
- pyflashdb-2.0.0/pyflashdb/export.py +117 -0
- pyflashdb-2.0.0/pyflashdb/filters.py +262 -0
- pyflashdb-2.0.0/pyflashdb/plugins.py +135 -0
- pyflashdb-2.0.0/pyflashdb/pool.py +118 -0
- pyflashdb-2.0.0/pyflashdb/schema.py +254 -0
- pyflashdb-2.0.0/pyflashdb/triggers.py +242 -0
- pyflashdb-2.0.0/pyflashdb/urls.py +90 -0
- pyflashdb-2.0.0/pyflashdb.egg-info/PKG-INFO +440 -0
- pyflashdb-2.0.0/pyflashdb.egg-info/SOURCES.txt +43 -0
- pyflashdb-2.0.0/pyflashdb.egg-info/requires.txt +25 -0
- pyflashdb-2.0.0/pyflashdb.egg-info/top_level.txt +2 -0
- pyflashdb-2.0.0/pyproject.toml +85 -0
- pyflashdb-2.0.0/tests/conftest.py +61 -0
- pyflashdb-2.0.0/tests/support.py +73 -0
- pyflashdb-2.0.0/tests/test_copy.py +63 -0
- pyflashdb-2.0.0/tests/test_dialects_schema.py +225 -0
- pyflashdb-2.0.0/tests/test_filters.py +190 -0
- pyflashdb-2.0.0/tests/test_mongo_unit.py +120 -0
- pyflashdb-2.0.0/tests/test_parity.py +443 -0
- pyflashdb-2.0.0/tests/test_sqlite_and_internals.py +300 -0
- pyflashdb-2.0.0/tests/test_urls_and_shim.py +88 -0
- pyflashdb-1.0.4/MANIFEST.in +0 -6
- pyflashdb-1.0.4/PKG-INFO +0 -236
- pyflashdb-1.0.4/README.md +0 -222
- pyflashdb-1.0.4/flash/__init__.py +0 -33
- pyflashdb-1.0.4/flash/adapters/mongo_adapter.py +0 -247
- pyflashdb-1.0.4/flash/adapters/mysql_adapter.py +0 -237
- pyflashdb-1.0.4/flash/adapters/postgres_adapter.py +0 -258
- pyflashdb-1.0.4/flash/core.py +0 -323
- pyflashdb-1.0.4/flash/exceptions.py +0 -48
- pyflashdb-1.0.4/flash/filters.py +0 -217
- pyflashdb-1.0.4/flash/triggers.py +0 -191
- pyflashdb-1.0.4/pyflashdb.egg-info/PKG-INFO +0 -236
- pyflashdb-1.0.4/pyflashdb.egg-info/SOURCES.txt +0 -18
- pyflashdb-1.0.4/pyflashdb.egg-info/top_level.txt +0 -3
- pyflashdb-1.0.4/pyproject.toml +0 -17
- pyflashdb-1.0.4/setup.py +0 -40
- /pyflashdb-1.0.4/flash/adapters/__init__.py → /pyflashdb-2.0.0/pyflashdb/py.typed +0 -0
- {pyflashdb-1.0.4 → pyflashdb-2.0.0}/pyflashdb.egg-info/dependency_links.txt +0 -0
- {pyflashdb-1.0.4 → pyflashdb-2.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v2.0.0 — Same behavior, any database
|
|
4
|
+
|
|
5
|
+
The headline of 2.0 is the library's own promise — *same code, same behavior, any database* — made
|
|
6
|
+
true, and tested: one parity suite (`tests/test_parity.py`) now runs identical assertions against
|
|
7
|
+
MySQL, PostgreSQL, SQLite and MongoDB in CI.
|
|
8
|
+
|
|
9
|
+
### Breaking changes
|
|
10
|
+
|
|
11
|
+
- **Import name is now `pyflashdb`** (`from pyflashdb import FlashDB`). `flash` was too generic a
|
|
12
|
+
top-level name. `import flash` still works as a **deprecated alias** (emits `DeprecationWarning`,
|
|
13
|
+
shares the same classes and exceptions) and will be removed in a later release. The pip package name
|
|
14
|
+
is unchanged.
|
|
15
|
+
- **Python ≥ 3.9** (3.8 is end-of-life).
|
|
16
|
+
- **`before_*` hooks can abort an operation** by raising (as the README's validation example always
|
|
17
|
+
implied). Previously every hook exception was swallowed and `print`ed. Exceptions in `after_*`
|
|
18
|
+
hooks are now logged through `logging` (`pyflashdb.hooks`) instead of `print`ed.
|
|
19
|
+
- **Backend behaviour now matches across databases** (these change results on some backends):
|
|
20
|
+
- `like` is case-sensitive everywhere (it was case-insensitive on MySQL, and unanchored on MongoDB).
|
|
21
|
+
Use the new `ilike` for case-insensitive matching.
|
|
22
|
+
- `!=` and `not in` include rows where the field is NULL on SQL, as MongoDB always did.
|
|
23
|
+
- `order_by="-field"` really sorts descending on SQL (it silently sorted ascending).
|
|
24
|
+
NULLs sort first ascending / last descending on every backend.
|
|
25
|
+
- `update()` returns rows *matched* on every backend (MySQL returned rows *changed*, MongoDB
|
|
26
|
+
`modified_count`).
|
|
27
|
+
- MongoDB `select(fields=[...])` no longer returns `_id` unless asked.
|
|
28
|
+
- `limit=0` returns no rows on every backend (MongoDB treated it as "no limit").
|
|
29
|
+
- Unknown filter operators — including Mongo-style `{"$gt": …}` — raise `FilterError`; they used to
|
|
30
|
+
be pasted into the SQL statement.
|
|
31
|
+
- **`describe()`** returns `[{"name", "type", "nullable", "default", "primary_key"}]` on every backend
|
|
32
|
+
instead of each driver's native shape.
|
|
33
|
+
- **New tables**: `float` is now double precision (`DOUBLE` / `DOUBLE PRECISION`; it was single
|
|
34
|
+
precision `FLOAT` / `REAL` and lost precision on round trips); `timestamp` is `DATETIME(6)` on MySQL;
|
|
35
|
+
text columns on MySQL use a binary collation so `=` is case-sensitive like on the other backends;
|
|
36
|
+
the primary-key column only auto-increments when it is an integer.
|
|
37
|
+
- **PostgreSQL `truncate()`** no longer uses `CASCADE` (it silently emptied other tables). It still
|
|
38
|
+
restarts identity counters.
|
|
39
|
+
- Records passed to **`bulk_insert`** are matched to columns by name (values used to be paired by
|
|
40
|
+
position, which corrupted data when dicts had different key order).
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **SQLite backend** — `FlashDB.from_url("sqlite:///app.db")`. Zero setup, standard library only.
|
|
45
|
+
- **`upsert()` / `upsert_many()`** — `ON DUPLICATE KEY UPDATE` (MySQL), `ON CONFLICT … DO UPDATE`
|
|
46
|
+
(PostgreSQL, SQLite), `update_one(…, upsert=True)` (MongoDB). `update_fields` limits what is overwritten.
|
|
47
|
+
- **`ilike`** operator; `like` escapes (`\%`, `\_`); the `.` in `"%@gmail.com"` is a literal dot on MongoDB.
|
|
48
|
+
- **`copy_to()`** — copy tables to another FlashDB, across backends, in batches (`append` / `replace` /
|
|
49
|
+
`upsert`); creates missing destination tables from the source schema.
|
|
50
|
+
- **`from_url()` / `from_env()`** and connection URLs (`mysql://`, `postgresql://`, `sqlite://`,
|
|
51
|
+
`mongodb://`, `mongodb+srv://`); **`echo=True`**.
|
|
52
|
+
- **`transaction()`** context manager (commit on success, rollback on exception; nestable).
|
|
53
|
+
- **MongoDB transactions work** and fail clearly: `begin()` detects a standalone server and raises a
|
|
54
|
+
`TransactionError` explaining how to run a replica set. (In 1.x the session was never passed to the
|
|
55
|
+
operations, so `rollback()` did nothing.)
|
|
56
|
+
- **`iter()`** — stream a table in batches via server-side cursors (MySQL, PostgreSQL) / cursors (MongoDB, SQLite).
|
|
57
|
+
- **Export**: `to_df()` (`pip install pyflashdb[pandas]`), `to_csv()`, `to_json()` (array or JSON Lines).
|
|
58
|
+
- **Connection pooling** (`pool_size`), **automatic reconnect** with read retries (`retries`,
|
|
59
|
+
`retry_delay`, `ping_after`), thread-safe use, per-thread transactions.
|
|
60
|
+
- **Richer schema**: `float`, `double`, `decimal(p,s)`, `bool`, `date`, `datetime`, `json`, `blob`, `str(n)`;
|
|
61
|
+
constraints `primary_key`, `unique`, `nullable`, `default`, `collation`; `create_index()` on every
|
|
62
|
+
backend, `table_exists()`, `add_column()`.
|
|
63
|
+
- **Hooks**: `before_*` / `after_*` for `select`, `upsert` and `count` as well as insert/update/delete;
|
|
64
|
+
`"*"` wildcard table; global `before_query` / `after_query` (statement, params, duration, row count);
|
|
65
|
+
`before_select` may rewrite the query.
|
|
66
|
+
- **Built-in plugins**: `Timestamps` (created_at / updated_at) and `AuditLog`.
|
|
67
|
+
- **`raw(sql, params)`** with a single `%s` placeholder style on all SQL backends (SQLite included).
|
|
68
|
+
- Full **type hints** and a **`py.typed`** marker.
|
|
69
|
+
- Parity test suite, cross-database copy tests, unit tests, GitHub Actions CI (MySQL, PostgreSQL,
|
|
70
|
+
MongoDB replica set, SQLite; Python 3.9–3.13), and a `docker-compose.yml` for local runs.
|
|
71
|
+
- **New manual** — `pyflashdb_v_2.0.0_README.pdf` replaces the 1.0.4 manual (same design): all 2.0 features,
|
|
72
|
+
the cross-database behavior contract, production use, an "Upgrading from 1.x" guide, and the 2.0 release notes.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **SQL injection paths closed**: table, column and index names are always quoted; filter operators
|
|
77
|
+
and column types are validated instead of interpolated; `LIMIT`/`OFFSET` must be integers.
|
|
78
|
+
- `OFFSET` without `LIMIT` now works on MySQL and SQLite.
|
|
79
|
+
- `raw()` on SQL returned the row *count* instead of `[]` for a SELECT with no rows.
|
|
80
|
+
- PostgreSQL `add()` assumed the key column was called `id`; it now looks the key up (and returns
|
|
81
|
+
`None` for key-less tables instead of failing).
|
|
82
|
+
- MySQL `TINYINT(1)` columns come back as `bool` and `JSON` columns as Python objects; PostgreSQL
|
|
83
|
+
`bytea` comes back as `bytes`; MongoDB `Decimal128` as `Decimal`; timezone-aware datetimes are stored
|
|
84
|
+
as naive UTC on every backend.
|
|
85
|
+
- `update()` with no fields, and empty `IN ()` lists, no longer generate invalid SQL/queries.
|
|
86
|
+
- `bulk_insert` / `upsert_many` on SQL are atomic.
|
|
87
|
+
|
|
88
|
+
### Packaging
|
|
89
|
+
|
|
90
|
+
- Metadata moved to `pyproject.toml` (PEP 621); `setup.py` removed. The previous
|
|
91
|
+
`build-backend = "setuptools.backends.legacy:build"` did not exist, so building from source failed.
|
|
92
|
+
- Optional extras: `mysql`, `postgres`, `mongodb`, `pandas`, `all`, `dev`. SQLite needs no extra.
|
|
93
|
+
- Release workflow publishes to PyPI via trusted publishing when a GitHub release is published.
|
|
94
|
+
|
|
95
|
+
### Not in 2.0
|
|
96
|
+
|
|
97
|
+
- An `AsyncFlashDB` (asyncpg / aiomysql / motor) is planned for a later release.
|
|
98
|
+
|
|
99
|
+
## v1.0.4 — Bug Fix Release
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **`FlashConnectionError` rename** (`flash/__init__.py`, `flash/exceptions.py`)
|
|
104
|
+
`ConnectionError` renamed to `FlashConnectionError` throughout the public API.
|
|
105
|
+
The old name silently shadowed Python's built-in `ConnectionError`, breaking
|
|
106
|
+
any code that caught real OS-level networking errors after importing from Flash.
|
|
107
|
+
A private backwards-compatibility alias remains in `flash.exceptions` only.
|
|
108
|
+
|
|
109
|
+
- **PostgreSQL `add()` return type** (`flash/adapters/postgres_adapter.py`)
|
|
110
|
+
`add()` now returns an `int` primary key (consistent with the MySQL adapter)
|
|
111
|
+
instead of the entire inserted row as a dict. Changed `RETURNING *` to
|
|
112
|
+
`RETURNING id` (or the configured `primary_key`).
|
|
113
|
+
|
|
114
|
+
- **`like` filter on MongoDB** (`flash/filters.py`)
|
|
115
|
+
SQL LIKE wildcards (`%` → any chars, `_` → one char) are now correctly
|
|
116
|
+
converted to regex equivalents (`.*` and `.`) and regex meta-characters in
|
|
117
|
+
the pattern are escaped before the value is sent to MongoDB's `$regex`.
|
|
118
|
+
Previously `%` was passed verbatim and matched nothing.
|
|
119
|
+
|
|
120
|
+
- **`__version__` corrected** (`flash/__init__.py`)
|
|
121
|
+
Updated from `"1.0.0"` to `"1.0.4"` to match the PyPI release.
|
|
122
|
+
|
|
123
|
+
- **All exceptions exported** (`flash/__init__.py`)
|
|
124
|
+
`TransactionError`, `SchemaError`, and `UnsupportedDatabaseError` are now
|
|
125
|
+
exported directly from `flash` in addition to `flash.exceptions`.
|
|
126
|
+
|
|
127
|
+
- **`raw()` routing fixed** (`flash/core.py`)
|
|
128
|
+
MongoDB's `table` keyword argument is now correctly forwarded to the adapter.
|
|
129
|
+
SQL adapters receive `(sql, params)`; the MongoDB adapter receives
|
|
130
|
+
`(command, table=table)`. Previously `table` was silently ignored for all
|
|
131
|
+
database types.
|
|
132
|
+
|
|
133
|
+
## v1.0.3
|
|
134
|
+
|
|
135
|
+
- Initial public PyPI release.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Aman
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Aman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
pyflashdb-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyflashdb
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: One API for MySQL, PostgreSQL, SQLite and MongoDB — same code, same behavior, any database.
|
|
5
|
+
Author: Aman Singh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AmanDev17/pyflashdb
|
|
8
|
+
Project-URL: Issues, https://github.com/AmanDev17/pyflashdb/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/pyflashdb/
|
|
11
|
+
Keywords: database,mysql,postgresql,sqlite,mongodb,crud,orm,unified,upsert,pyflashdb
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: mysql
|
|
28
|
+
Requires-Dist: mysql-connector-python>=8.0; extra == "mysql"
|
|
29
|
+
Provides-Extra: postgres
|
|
30
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
|
|
31
|
+
Provides-Extra: mongodb
|
|
32
|
+
Requires-Dist: pymongo>=4.0; extra == "mongodb"
|
|
33
|
+
Provides-Extra: pandas
|
|
34
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: mysql-connector-python>=8.0; extra == "all"
|
|
37
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == "all"
|
|
38
|
+
Requires-Dist: pymongo>=4.0; extra == "all"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
41
|
+
Requires-Dist: pandas>=1.5; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
44
|
+
Requires-Dist: build; extra == "dev"
|
|
45
|
+
Requires-Dist: twine; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
<div align="center">
|
|
49
|
+
|
|
50
|
+
<br/>
|
|
51
|
+
|
|
52
|
+
# ⚡ pyflashdb
|
|
53
|
+
|
|
54
|
+
**Same code. Same behavior. Any database.**
|
|
55
|
+
|
|
56
|
+
[](https://github.com/AmanDev17/pyflashdb/actions/workflows/ci.yml)
|
|
57
|
+
[](https://github.com/AmanDev17/pyflashdb/blob/main/tests/test_parity.py)
|
|
58
|
+
[](https://pypi.org/project/pyflashdb/)
|
|
59
|
+
[](https://www.python.org/)
|
|
60
|
+
[](https://github.com/AmanDev17/pyflashdb/blob/main/LICENSE)
|
|
61
|
+
[](https://pepy.tech/projects/pyflashdb)
|
|
62
|
+
|
|
63
|
+
<br/>
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## What is pyflashdb?
|
|
70
|
+
|
|
71
|
+
pyflashdb lets you talk to **MySQL**, **PostgreSQL**, **SQLite** and **MongoDB** with one API — and
|
|
72
|
+
the *same* filters, ordering, null handling, upserts and transactions give the *same* results on
|
|
73
|
+
each of them. No SQL required, no driver switching, no boilerplate.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pyflashdb import FlashDB
|
|
77
|
+
|
|
78
|
+
with FlashDB.from_url("sqlite:///app.db") as flash: # or postgresql://…, mysql://…, mongodb://…
|
|
79
|
+
flash.add("users", {"name": "Alice", "age": 25}) # insert
|
|
80
|
+
flash.where("users", {"age": {">": 18}}) # filter
|
|
81
|
+
flash.update("users", {"name": "Alice"}, {"age": 26}) # update
|
|
82
|
+
flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
|
|
83
|
+
flash.delete("users", {"name": "Alice"}) # delete
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Change the URL and nothing else changes.
|
|
87
|
+
|
|
88
|
+
> **The claim is tested.** [`tests/test_parity.py`](https://github.com/AmanDev17/pyflashdb/blob/main/tests/test_parity.py)
|
|
89
|
+
> runs one set of assertions against real MySQL, PostgreSQL, MongoDB and SQLite in CI. If any
|
|
90
|
+
> backend returns different rows, that backend's run fails.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install pyflashdb # SQLite works out of the box — no driver needed
|
|
98
|
+
pip install "pyflashdb[mysql]" # + MySQL (mysql-connector-python)
|
|
99
|
+
pip install "pyflashdb[postgres]" # + PostgreSQL (psycopg2-binary)
|
|
100
|
+
pip install "pyflashdb[mongodb]" # + MongoDB (pymongo)
|
|
101
|
+
pip install "pyflashdb[all]" # all three drivers
|
|
102
|
+
pip install "pyflashdb[pandas]" # + flash.to_df()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The package is `pyflashdb` and it is imported as `pyflashdb`. (The old `import flash` still works in
|
|
106
|
+
2.x but is deprecated — see [Upgrading](#upgrading-from-1x).)
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Connect
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import os
|
|
114
|
+
from pyflashdb import FlashDB
|
|
115
|
+
|
|
116
|
+
flash = FlashDB.from_url(os.environ["DATABASE_URL"]) # passwords stay out of your code
|
|
117
|
+
flash = FlashDB.from_env() # same, reads $DATABASE_URL
|
|
118
|
+
flash = FlashDB.from_url("postgresql://user:pass@localhost:5432/app", echo=True) # print every query
|
|
119
|
+
|
|
120
|
+
flash = FlashDB("mysql", {"host": "localhost", "user": "root", "password": "…", "database": "app"})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Backend | URL | Config keys (dict form) |
|
|
124
|
+
| ---------- | ------------------------------------------------ | ----------------------------------------------- |
|
|
125
|
+
| MySQL | `mysql://user:pass@host:3306/db` | host, port, user, password, database, charset |
|
|
126
|
+
| PostgreSQL | `postgresql://user:pass@host:5432/db` | host, port, user, password, database |
|
|
127
|
+
| SQLite | `sqlite:///app.db` · `sqlite:///:memory:` | database |
|
|
128
|
+
| MongoDB | `mongodb://user:pass@host:27017/db` · `mongodb+srv://…` | uri, host, port, user, password, database |
|
|
129
|
+
|
|
130
|
+
Options (as URL query string or config keys): `pool_size` (connections, default 1), `pool_timeout`,
|
|
131
|
+
`retries`, `retry_delay`, `ping_after`, `connect_args` (passed to the driver: ssl, …). A pre-built
|
|
132
|
+
`pymongo.MongoClient` can be supplied as `config["client"]`.
|
|
133
|
+
|
|
134
|
+
Always use it as a context manager (or call `flash.close()`):
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
with FlashDB.from_url(url) as flash:
|
|
138
|
+
...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Everyday API
|
|
144
|
+
|
|
145
|
+
| Method | What it does |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `add(table, data)` | Insert one record, returns its primary key |
|
|
148
|
+
| `bulk_insert(table, records)` | Insert many records (atomic on SQL); records may have different keys |
|
|
149
|
+
| `all` · `select` · `where` · `find_one` | Read, with `fields`, `filters`, `order_by`, `limit`, `offset` |
|
|
150
|
+
| `count` · `paginate` | Count / one page + total |
|
|
151
|
+
| `iter(table, batch_size=1000)` | Stream a big table in batches |
|
|
152
|
+
| `update(table, filters, data)` | Update matching records → rows *matched* |
|
|
153
|
+
| `upsert` · `upsert_many` | Insert, or update if the key exists |
|
|
154
|
+
| `delete(table, filters)` | Delete matching records |
|
|
155
|
+
| `create_table` · `drop_table` · `truncate` · `add_column` · `create_index` | Schema |
|
|
156
|
+
| `table_exists` · `show_tables` · `describe` | Introspection (same shape on every backend) |
|
|
157
|
+
| `begin` · `commit` · `rollback` · `transaction()` | Transactions |
|
|
158
|
+
| `copy_to(other, tables=[…])` | Copy tables to another database, across backends |
|
|
159
|
+
| `to_df` · `to_csv` · `to_json` | Export |
|
|
160
|
+
| `raw(query, params)` | Escape hatch |
|
|
161
|
+
|
|
162
|
+
### Filters — write once, same result everywhere
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
flash.where("users", {"age": {">=": 18, "<=": 65}})
|
|
166
|
+
flash.where("users", {"role": {"in": ["admin", "mod"]}})
|
|
167
|
+
flash.where("users", {"status": {"!=": "banned"}})
|
|
168
|
+
flash.where("users", {"email": {"like": "%@gmail.com"}}) # case-SENSITIVE
|
|
169
|
+
flash.where("users", {"email": {"ilike": "%@gmail.com"}}) # case-insensitive
|
|
170
|
+
flash.where("users", {"deleted_at": None}) # IS NULL / missing
|
|
171
|
+
flash.select("users", fields=["name", "age"], order_by=["-age", "name"], limit=10, offset=20)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
These rules hold on **every** backend (they are what the parity suite checks):
|
|
175
|
+
|
|
176
|
+
* `like` is **case-sensitive** and **anchored**: `%` = any run of characters, `_` = one character,
|
|
177
|
+
`\%` / `\_` = literal. Every other character is literal, so the `.` in `%@gmail.com` is a dot.
|
|
178
|
+
`ilike` is the case-insensitive form.
|
|
179
|
+
* `None` matches NULL/missing. `!=` and `not in` **include** NULLs (plain SQL would drop them).
|
|
180
|
+
`in []` matches nothing; `not in []` matches everything.
|
|
181
|
+
* `order_by="-age"` sorts descending; NULLs sort first ascending, last descending.
|
|
182
|
+
* `update()` returns rows **matched**, even if a value did not change.
|
|
183
|
+
* Unknown operators raise `FilterError` — nothing user-supplied is ever pasted into SQL, and every
|
|
184
|
+
table/column name is quoted.
|
|
185
|
+
|
|
186
|
+
### Return types
|
|
187
|
+
|
|
188
|
+
| | `add()` returns | rows contain |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| MySQL · PostgreSQL · SQLite | `int` — the new primary key (your own value if you supplied the key; `None` for a table with no key) | `id` |
|
|
191
|
+
| MongoDB | `str` — the new `_id` as hex | `_id` (a `str`) |
|
|
192
|
+
|
|
193
|
+
The returned id can be used straight in a filter on every backend:
|
|
194
|
+
`flash.find_one("users", {"id": new_id})` (SQL) / `{"_id": new_id}` (Mongo — a 24-hex string is
|
|
195
|
+
treated as an ObjectId). Values come back as native Python types everywhere: `bool`, `datetime`
|
|
196
|
+
(naive UTC), `Decimal`, and `dict`/`list` for `json` columns.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Upsert
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
flash.upsert("users", {"email": "a@x.com", "name": "Alice"}, key="email")
|
|
204
|
+
flash.upsert("users", data, key=["org_id", "email"], update_fields=["name"]) # only overwrite `name`
|
|
205
|
+
flash.upsert_many("users", rows, key="email")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Backend | Native mechanism |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| MySQL | `ON DUPLICATE KEY UPDATE` |
|
|
211
|
+
| PostgreSQL · SQLite | `ON CONFLICT (...) DO UPDATE` |
|
|
212
|
+
| MongoDB | `update_one(..., upsert=True)` (`bulk_write` for many) |
|
|
213
|
+
|
|
214
|
+
On SQL the `key` must be backed by a primary key or unique index (`{"unique": True}` in
|
|
215
|
+
`create_table`). On MySQL the update fires on *any* unique-key conflict.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Transactions
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
with flash.transaction(): # commits on success, rolls back on any exception
|
|
223
|
+
flash.update("accounts", {"user_id": 1}, {"balance": 400})
|
|
224
|
+
flash.update("accounts", {"user_id": 2}, {"balance": 600})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`begin()` / `commit()` / `rollback()` still exist. A `transaction()` inside another one joins it.
|
|
228
|
+
Transactions are per thread.
|
|
229
|
+
|
|
230
|
+
**MongoDB needs a replica set.** On a standalone `mongod` (what most people run locally) `begin()`
|
|
231
|
+
raises a `TransactionError` explaining how to fix it, instead of failing confusingly. For local
|
|
232
|
+
development run a single-node replica set — `docker compose up -d` in this repository does that — and
|
|
233
|
+
connect with `?replicaSet=rs0`. Atlas works out of the box.
|
|
234
|
+
|
|
235
|
+
(MySQL DDL such as `TRUNCATE` / `ALTER TABLE` commits implicitly, as in MySQL itself.)
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Hooks & plugins
|
|
240
|
+
|
|
241
|
+
Before/after hooks for every data operation — also on MongoDB, which has no triggers:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
@flash.before_insert("users")
|
|
245
|
+
def validate(data): # a dict for add(), a list of dicts for bulk_insert()
|
|
246
|
+
if not data.get("email"):
|
|
247
|
+
raise ValueError("email required") # aborts the insert
|
|
248
|
+
|
|
249
|
+
@flash.after_insert("users")
|
|
250
|
+
def on_created(data, result):
|
|
251
|
+
print("new id:", result)
|
|
252
|
+
|
|
253
|
+
@flash.before_select("orders") # may rewrite the query — e.g. multi-tenant scoping
|
|
254
|
+
def scope(payload):
|
|
255
|
+
payload["filters"] = {**(payload["filters"] or {}), "tenant_id": 7}
|
|
256
|
+
|
|
257
|
+
@flash.before_query # sees every statement sent to the database
|
|
258
|
+
def log_sql(info):
|
|
259
|
+
print(info["backend"], info["statement"], info["params"])
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Events: `insert`, `update`, `delete`, `select`, `upsert`, `count` — each with `before_*` / `after_*`
|
|
263
|
+
(`"*"` hooks every table) — plus `before_query` / `after_query`. `before` hooks can modify the payload
|
|
264
|
+
and **abort** by raising; an exception in an `after` hook is logged, not raised.
|
|
265
|
+
|
|
266
|
+
Built-in plugins:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from pyflashdb.plugins import Timestamps, AuditLog
|
|
270
|
+
|
|
271
|
+
Timestamps(["users", "orders"]).install(flash) # created_at / updated_at maintained for you
|
|
272
|
+
AuditLog(["users"]).install(flash) # logs every write to the `pyflashdb.audit` logger
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Schema
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
flash.create_table("users", {
|
|
281
|
+
"id": "int", # auto-increment primary key
|
|
282
|
+
"email": {"type": "str", "unique": True, "nullable": False},
|
|
283
|
+
"name": "str(100)", # VARCHAR(100)
|
|
284
|
+
"score": {"type": "float", "default": 0},
|
|
285
|
+
"balance": "decimal(10,2)",
|
|
286
|
+
"active": {"type": "bool", "default": True},
|
|
287
|
+
"meta": "json",
|
|
288
|
+
"joined": "datetime",
|
|
289
|
+
})
|
|
290
|
+
flash.add_column("users", "nickname", {"type": "str", "nullable": True})
|
|
291
|
+
flash.create_index("users", ["name", "joined"]) # idempotent; unique=True for a unique index
|
|
292
|
+
flash.table_exists("users")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Types: `int` `bigint` `smallint` `str` `text` `float` `double` `decimal` `bool` `date` `datetime`
|
|
296
|
+
`timestamp` `json` `blob`. Spec keys: `type` `length` `precision` `scale` `primary_key`
|
|
297
|
+
`autoincrement` `unique` `nullable` `default` `collation`. An unrecognised type is passed through only
|
|
298
|
+
if it looks like a plain type name (`BIGINT`, `VARCHAR(50)`); anything else raises `SchemaError`.
|
|
299
|
+
|
|
300
|
+
On MongoDB, `create_table` applies a `$jsonSchema` validator (types, `nullable: False` → required)
|
|
301
|
+
and unique indexes; server-side `default`s don't exist there and are ignored with a warning.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Large tables, export and copying between databases
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
for batch in flash.iter("orders", batch_size=1000): # server-side cursor — never loads it all
|
|
309
|
+
process(batch)
|
|
310
|
+
|
|
311
|
+
flash.to_csv("users", "users.csv", fields=["name", "email"])
|
|
312
|
+
flash.to_json("users", "users.json", indent=2) # lines=True for JSON Lines
|
|
313
|
+
df = flash.to_df("users", filters={"age": {">": 18}}) # pip install "pyflashdb[pandas]"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Copy between databases** — "MySQL locally, PostgreSQL in production", made literal:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
local = FlashDB.from_url("mysql://root:pw@localhost/app")
|
|
320
|
+
prod = FlashDB.from_url(os.environ["PROD_DATABASE_URL"])
|
|
321
|
+
|
|
322
|
+
local.copy_to(prod, tables=["users", "orders"], batch_size=1000)
|
|
323
|
+
# {'users': 1200, 'orders': 8431}
|
|
324
|
+
local.copy_to(prod, tables=["users"], mode="upsert", key="email") # or mode="replace"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Missing destination tables are created from the source's columns, types, nullability and primary
|
|
328
|
+
key (indexes, defaults and foreign keys are not copied). Ids are copied as-is between SQL databases
|
|
329
|
+
(PostgreSQL counters are re-synced). Between SQL and MongoDB each side keeps its own key.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Production
|
|
334
|
+
|
|
335
|
+
* **Pooling** — `pool_size=10` (MySQL, PostgreSQL). One shared pool implementation; MongoDB's driver
|
|
336
|
+
pools by itself. The default (`1`) is one connection shared safely across threads.
|
|
337
|
+
* **Reconnect & retry** — idle connections are health-checked, dead ones replaced. If a connection
|
|
338
|
+
drops mid-*read* the read is retried (`retries=2`). Writes are never retried blindly, because the
|
|
339
|
+
server may already have applied them; the connection is replaced so the next call works.
|
|
340
|
+
* **Observability** — `echo=True`, `before_query` / `after_query` hooks (with duration and row
|
|
341
|
+
count), and the `pyflashdb.query` / `pyflashdb.audit` / `pyflashdb.hooks` loggers.
|
|
342
|
+
* **Typed** — full type hints and a `py.typed` marker, so IDE completion and mypy work.
|
|
343
|
+
* **`raw()`** — `flash.raw("SELECT * FROM users WHERE age > %s", [18])`. The placeholder is `%s` on
|
|
344
|
+
every SQL backend, SQLite included (write `%%` for a literal percent sign when passing params).
|
|
345
|
+
Statements that return rows give a `list[dict]` (possibly empty); others give the row count.
|
|
346
|
+
MongoDB: `flash.raw({"age": {"$gt": 18}}, table="users")` or `flash.raw("dbStats")`.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Known differences
|
|
351
|
+
|
|
352
|
+
pyflashdb removes the differences that can be removed. These remain, by nature of the databases:
|
|
353
|
+
|
|
354
|
+
* **Ids**: `int` vs `str` (above). MongoDB documents contain only the fields you stored — a missing
|
|
355
|
+
field is not turned into `None`.
|
|
356
|
+
* **String collation**: tables created by `create_table` use a binary collation on MySQL, so `=`,
|
|
357
|
+
`IN` and `LIKE` are case-sensitive like everywhere else. On tables you created yourself, `=` follows
|
|
358
|
+
the column's own collation (`like` / `ilike` are correct regardless). Sort order of non-ASCII text
|
|
359
|
+
and non-ASCII case folding for `ilike` follow each database's locale.
|
|
360
|
+
* **SQLite** stores `decimal` as a numeric value (precision beyond ~15 digits is not guaranteed) and
|
|
361
|
+
has a single writer, so it uses one connection.
|
|
362
|
+
* **MongoDB**: transactions need a replica set; `bulk_insert` is not atomic outside a transaction; no
|
|
363
|
+
column defaults.
|
|
364
|
+
* **MySQL**: `upsert` fires on any unique key; `JSON` columns need MySQL ≥ 5.7.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Upgrading from 1.x
|
|
369
|
+
|
|
370
|
+
2.0 is a major release. What to change:
|
|
371
|
+
|
|
372
|
+
* **Import `pyflashdb`**: `from pyflashdb import FlashDB`. `import flash` still works for now but
|
|
373
|
+
warns and will be removed in a later release.
|
|
374
|
+
* **Python ≥ 3.9** (3.8 is end-of-life).
|
|
375
|
+
* **`before_*` hooks can now abort** (raise to cancel the operation) — in 1.x exceptions were
|
|
376
|
+
swallowed and printed. `after_*` hook errors are logged via `logging`, no longer `print`ed.
|
|
377
|
+
* **`describe()`** returns `[{"name", "type", "nullable", "default", "primary_key"}]` on every backend.
|
|
378
|
+
* **`!=` / `not in`** now include NULLs on SQL (as MongoDB always did); **`like`** is case-sensitive on
|
|
379
|
+
MySQL (it was case-insensitive); **`order_by="-x"`** now really sorts descending on SQL;
|
|
380
|
+
**`update()`** returns rows matched (MySQL used to return rows changed); Mongo `select(fields=…)`
|
|
381
|
+
no longer includes `_id`.
|
|
382
|
+
* **Unknown filter operators** (including raw `$gt`-style operators on Mongo) raise `FilterError`.
|
|
383
|
+
* **`float` columns** are now double precision (`DOUBLE` / `DOUBLE PRECISION`), and `timestamp`
|
|
384
|
+
columns on MySQL are `DATETIME(6)`.
|
|
385
|
+
* **PostgreSQL `truncate()`** no longer cascades into other tables.
|
|
386
|
+
* `create_index()` now works on every backend (it was MongoDB-only).
|
|
387
|
+
|
|
388
|
+
Full list: [Changelog](https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md).
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## How pyflashdb compares
|
|
393
|
+
|
|
394
|
+
| | pyflashdb | SQLAlchemy | Django ORM | Raw drivers |
|
|
395
|
+
| ------------------------- | :-------: | :--------: | :--------: | :---------: |
|
|
396
|
+
| MySQL / PostgreSQL | ✅ | ✅ | ✅ | ✅ |
|
|
397
|
+
| SQLite | ✅ | ✅ | ✅ | ✅ |
|
|
398
|
+
| MongoDB | ✅ | ❌ | ❌ | ✅ |
|
|
399
|
+
| Same API across all DBs | ✅ | ❌ | ❌ | ❌ |
|
|
400
|
+
| Copy tables between DBs | ✅ | ❌ | ❌ | ❌ |
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Development
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pip install -e ".[all,dev]"
|
|
408
|
+
pytest # SQLite + unit tests; MySQL/PostgreSQL/Mongo are skipped unless configured
|
|
409
|
+
docker compose up -d --wait # start MySQL, PostgreSQL and a MongoDB replica set, then export:
|
|
410
|
+
export PYFLASHDB_TEST_MYSQL_URL=mysql://root:flash@127.0.0.1:3306/flash_test
|
|
411
|
+
export PYFLASHDB_TEST_POSTGRES_URL=postgresql://postgres:flash@127.0.0.1:5432/flash_test
|
|
412
|
+
export PYFLASHDB_TEST_MONGODB_URL='mongodb://127.0.0.1:27017/flash_test?replicaSet=rs0'
|
|
413
|
+
pytest -ra # the full parity suite
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Documentation
|
|
417
|
+
|
|
418
|
+
📄 [Full Documentation (PDF, v2.0.0)](https://github.com/AmanDev17/pyflashdb/blob/main/pyflashdb_v_2.0.0_README.pdf) —
|
|
419
|
+
a 39-page manual covering every method, the cross-database behavior contract, production use, upgrading from 1.x, and
|
|
420
|
+
the full list of what's new in 2.0 (last chapter).
|
|
421
|
+
|
|
422
|
+
## Links
|
|
423
|
+
|
|
424
|
+
* **PyPI:** https://pypi.org/project/pyflashdb/
|
|
425
|
+
* **Issues:** https://github.com/AmanDev17/pyflashdb/issues
|
|
426
|
+
* **Changelog:** https://github.com/AmanDev17/pyflashdb/blob/main/Changelog.md
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
<div align="center">
|
|
431
|
+
|
|
432
|
+
**pyflashdb v2.0.0** · MIT License · Python 3.9+
|
|
433
|
+
|
|
434
|
+
*One API. Any database. Same behavior.*
|
|
435
|
+
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Made possible because of Komal
|