SQLitey 0.1.0__tar.gz → 0.2.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.
- sqlitey-0.2.0/.github/workflows/ci.yaml +26 -0
- {sqlitey-0.1.0 → sqlitey-0.2.0}/.gitignore +3 -0
- sqlitey-0.2.0/Makefile +34 -0
- sqlitey-0.2.0/PKG-INFO +355 -0
- sqlitey-0.2.0/README.md +335 -0
- sqlitey-0.2.0/pyproject.toml +36 -0
- sqlitey-0.2.0/sqlitey/__init__.py +482 -0
- sqlitey-0.2.0/tests/__init__.py +0 -0
- sqlitey-0.2.0/tests/test_sqlitey.py +462 -0
- sqlitey-0.2.0/uv.lock +272 -0
- sqlitey-0.1.0/.github/workflows/ci.yaml +0 -22
- sqlitey-0.1.0/Makefile +0 -47
- sqlitey-0.1.0/PKG-INFO +0 -87
- sqlitey-0.1.0/README.md +0 -57
- sqlitey-0.1.0/pyproject.toml +0 -23
- sqlitey-0.1.0/sqlitey/__init__.py +0 -154
- sqlitey-0.1.0/tests/test_sqlitey.py +0 -180
- sqlitey-0.1.0/uv.lock +0 -789
- {sqlitey-0.1.0 → sqlitey-0.2.0}/.python-version +0 -0
- {sqlitey-0.1.0 → sqlitey-0.2.0}/LICENSE +0 -0
- /sqlitey-0.1.0/tests/__init__.py → /sqlitey-0.2.0/sqlitey/py.typed +0 -0
- {sqlitey-0.1.0 → sqlitey-0.2.0}/tests/sql/test.sql +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
- name: Set up uv with Python ${{ matrix.python-version }}
|
|
14
|
+
uses: astral-sh/setup-uv@v7
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync
|
|
19
|
+
- name: Lint with Ruff
|
|
20
|
+
run: uv run ruff check sqlitey tests
|
|
21
|
+
- name: Check formatting
|
|
22
|
+
run: uv run ruff format --check sqlitey tests
|
|
23
|
+
- name: Run Mypy
|
|
24
|
+
run: uv run mypy sqlitey
|
|
25
|
+
- name: Test suite
|
|
26
|
+
run: uv run pytest --cov=sqlitey tests
|
sqlitey-0.2.0/Makefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
SRC = sqlitey tests
|
|
2
|
+
|
|
3
|
+
.PHONY: help
|
|
4
|
+
help: ## Show this help menu
|
|
5
|
+
@echo "Usage: make [TARGET ...]"
|
|
6
|
+
@echo ""
|
|
7
|
+
@grep --no-filename -E '^[a-zA-Z_%-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
|
8
|
+
awk 'BEGIN {FS = ":.*?## "}; {printf "%-25s %s\n", $$1, $$2}'
|
|
9
|
+
|
|
10
|
+
.PHONY: deps
|
|
11
|
+
deps: ## Install requirements in virtual environment
|
|
12
|
+
uv sync
|
|
13
|
+
|
|
14
|
+
.PHONY: tests
|
|
15
|
+
tests: ## Run unit tests
|
|
16
|
+
uv run pytest --cov=sqlitey --cov-report term-missing tests
|
|
17
|
+
|
|
18
|
+
.PHONY: mypy
|
|
19
|
+
mypy: ## Run mypy
|
|
20
|
+
uv run mypy sqlitey
|
|
21
|
+
|
|
22
|
+
.PHONY: ruff
|
|
23
|
+
ruff: ## Run ruff linter
|
|
24
|
+
uv run ruff check $(SRC)
|
|
25
|
+
|
|
26
|
+
.PHONY: fmt
|
|
27
|
+
fmt: ## Format code with ruff
|
|
28
|
+
uv run ruff format $(SRC)
|
|
29
|
+
|
|
30
|
+
.PHONY: release
|
|
31
|
+
release: ## Release to PyPI
|
|
32
|
+
rm -rf dist
|
|
33
|
+
uv build
|
|
34
|
+
uv publish
|
sqlitey-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: SQLitey
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Useful wrapper around SQLite
|
|
5
|
+
Project-URL: Repository, https://github.com/smallwat3r/sqlitey
|
|
6
|
+
Project-URL: Issues, https://github.com/smallwat3r/sqlitey/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# SQLitey
|
|
22
|
+
|
|
23
|
+
A small, typed wrapper around Python's built-in `sqlite3`. It keeps you writing plain SQL, and takes care of the repetitive parts: connections, transactions, row shapes, SQL files and migrations.
|
|
24
|
+
|
|
25
|
+
- **No dependencies**, standard library only
|
|
26
|
+
- **Plain SQL**, inline or loaded from `.sql` files
|
|
27
|
+
- **Rows the way you want them**: tuples, dicts, namedtuples or your own dataclasses
|
|
28
|
+
- **Safe transactions**: commit on success, roll back on error, nestable atomic blocks
|
|
29
|
+
- **Migrations built in**, from numbered `.sql` files
|
|
30
|
+
- **Fully typed**, ships with type hints
|
|
31
|
+
|
|
32
|
+
Requires Python 3.11 or newer.
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Quick start](#quick-start)
|
|
38
|
+
- [Guide](#guide)
|
|
39
|
+
- [Writing queries](#writing-queries)
|
|
40
|
+
- [Reading results](#reading-results)
|
|
41
|
+
- [Row factories](#row-factories)
|
|
42
|
+
- [SQL files](#sql-files)
|
|
43
|
+
- [Transactions](#transactions)
|
|
44
|
+
- [Bulk inserts and scripts](#bulk-inserts-and-scripts)
|
|
45
|
+
- [Pragmas](#pragmas)
|
|
46
|
+
- [Migrations](#migrations)
|
|
47
|
+
- [Things to know](#things-to-know)
|
|
48
|
+
- [API reference](#api-reference)
|
|
49
|
+
- [Development](#development)
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pip install sqlitey
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from sqlitey import Db, Sql
|
|
61
|
+
|
|
62
|
+
with Db("app.db") as db:
|
|
63
|
+
db.execute(Sql.raw("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"))
|
|
64
|
+
db.execute(Sql.raw("INSERT INTO users (name) VALUES (?)"), ("Alice",))
|
|
65
|
+
|
|
66
|
+
user = db.fetchone(Sql.raw("SELECT id, name FROM users WHERE name = ?"), ("Alice",))
|
|
67
|
+
print(user) # (1, 'Alice')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That is all there is to it. When the `with` block ends, your changes are committed and the connection is closed. If an exception is raised inside the block, the changes are rolled back instead.
|
|
71
|
+
|
|
72
|
+
## Guide
|
|
73
|
+
|
|
74
|
+
### Writing queries
|
|
75
|
+
|
|
76
|
+
Every query is wrapped in a `Sql` object. Use `Sql.raw()` for inline SQL:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
get_user = Sql.raw("SELECT id, name FROM users WHERE id = ?")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pass values separately, as parameters. Both positional (`?`) and named (`:name`) placeholders work:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
db.fetchone(Sql.raw("SELECT * FROM users WHERE id = ?"), (1,))
|
|
86
|
+
db.fetchone(Sql.raw("SELECT * FROM users WHERE id = :id"), {"id": 1})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Never build a query by formatting values into the string, that is how SQL injection happens:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Don't do this
|
|
93
|
+
db.fetchone(Sql.raw(f"SELECT * FROM users WHERE name = '{name}'"))
|
|
94
|
+
|
|
95
|
+
# Do this
|
|
96
|
+
db.fetchone(Sql.raw("SELECT * FROM users WHERE name = ?"), (name,))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To help with that, `Sql.raw()` is typed as `LiteralString`. Type checkers that enforce it, such as pyright, report the first example above as an error. Note this is a static check only, nothing is verified at runtime.
|
|
100
|
+
|
|
101
|
+
### Reading results
|
|
102
|
+
|
|
103
|
+
| You want | Use | Returns |
|
|
104
|
+
|----------|-----|---------|
|
|
105
|
+
| One row | `db.fetchone(sql, params)` | A row, or `None` |
|
|
106
|
+
| All rows | `db.fetchall(sql, params)` | A list of rows |
|
|
107
|
+
| A single value | `db.fetchval(sql, params)` | The first column of the first row, or `None` |
|
|
108
|
+
| To loop over many rows | `db.execute(sql, params)` | A cursor you can iterate |
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
total = db.fetchval(Sql.raw("SELECT COUNT(*) FROM users")) # 2
|
|
112
|
+
|
|
113
|
+
for user_id, name in db.execute(Sql.raw("SELECT id, name FROM users")):
|
|
114
|
+
print(user_id, name)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Row factories
|
|
118
|
+
|
|
119
|
+
By default rows are tuples. Pass a `row_factory` to get something friendlier.
|
|
120
|
+
|
|
121
|
+
As dictionaries:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from sqlitey import Db, Sql, dict_factory
|
|
125
|
+
|
|
126
|
+
with Db("app.db", row_factory=dict_factory) as db:
|
|
127
|
+
user = db.fetchone(Sql.raw("SELECT id, name FROM users WHERE id = ?"), (1,))
|
|
128
|
+
print(user["name"]) # 'Alice'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
As namedtuples:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from sqlitey import Db, Sql, namedtuple_factory
|
|
135
|
+
|
|
136
|
+
with Db("app.db", row_factory=namedtuple_factory) as db:
|
|
137
|
+
user = db.fetchone(Sql.raw("SELECT id, name FROM users WHERE id = ?"), (1,))
|
|
138
|
+
print(user.name) # 'Alice'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
As your own classes, the column names must match the field names:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from dataclasses import dataclass
|
|
145
|
+
from sqlitey import Db, Sql, dataclass_factory
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class User:
|
|
149
|
+
id: int
|
|
150
|
+
name: str
|
|
151
|
+
|
|
152
|
+
with Db("app.db", row_factory=dataclass_factory(User)) as db:
|
|
153
|
+
user = db.fetchone(Sql.raw("SELECT id, name FROM users WHERE id = ?"), (1,))
|
|
154
|
+
print(user) # User(id=1, name='Alice')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
> **Tip:** with `namedtuple_factory`, a column such as `COUNT(*)` is not a valid attribute name, so it is renamed to its position (`_0`, `_1`, ...). Give it an alias to get a proper name: `SELECT COUNT(*) AS total`.
|
|
158
|
+
|
|
159
|
+
### SQL files
|
|
160
|
+
|
|
161
|
+
Longer queries are easier to read, and to syntax-highlight, in their own `.sql` files. Point `Db` at the directory and refer to queries by filename:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
sql/
|
|
165
|
+
get_user_by_id.sql
|
|
166
|
+
list_users.sql
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from pathlib import Path
|
|
171
|
+
from sqlitey import Db, Sql
|
|
172
|
+
|
|
173
|
+
with Db("app.db", sql_templates_dir=Path("sql")) as db:
|
|
174
|
+
user = db.fetchone(Sql.template("get_user_by_id.sql"), (1,))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A single query can also carry its own directory:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
sql = Sql.template("get_user_by_id.sql", path=Path("sql"))
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If you pass the database and the SQL directory around together, `DbPathConfig` bundles them:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from sqlitey import Db, DbPathConfig
|
|
187
|
+
|
|
188
|
+
config = DbPathConfig(database=Path("app.db"), sql_templates_dir=Path("sql"))
|
|
189
|
+
|
|
190
|
+
with Db.from_config(config) as db:
|
|
191
|
+
...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`Sql` objects are immutable, so you can define them once at the top of a module and reuse them everywhere.
|
|
195
|
+
|
|
196
|
+
### Transactions
|
|
197
|
+
|
|
198
|
+
Inside a `with Db(...)` block, your changes form one transaction:
|
|
199
|
+
|
|
200
|
+
- the block ends normally, everything is **committed**
|
|
201
|
+
- the block raises an exception, everything is **rolled back**
|
|
202
|
+
|
|
203
|
+
For finer control, wrap a group of statements in `db.transaction()`. They succeed or fail together, without affecting the rest of your work:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
with Db("app.db") as db:
|
|
207
|
+
db.execute(Sql.raw("INSERT INTO users (name) VALUES (?)"), ("Alice",))
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with db.transaction():
|
|
211
|
+
db.execute(Sql.raw("INSERT INTO users (name) VALUES (?)"), ("Bob",))
|
|
212
|
+
raise RuntimeError("something went wrong")
|
|
213
|
+
except RuntimeError:
|
|
214
|
+
pass # Bob is rolled back, Alice is kept
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`db.transaction()` blocks can be nested. When no transaction is open yet, the outermost block commits as soon as it succeeds.
|
|
218
|
+
|
|
219
|
+
Other options:
|
|
220
|
+
|
|
221
|
+
- `db.commit(sql, params)` runs a statement and commits straight away
|
|
222
|
+
- `Db("app.db", autocommit=True)` commits every statement as it runs
|
|
223
|
+
- without a `with` block, call `db.close()` yourself when done. Uncommitted changes are discarded, so commit first
|
|
224
|
+
|
|
225
|
+
### Bulk inserts and scripts
|
|
226
|
+
|
|
227
|
+
Run one statement with many sets of parameters:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
users = [("Alice",), ("Bob",), ("Charlie",)]
|
|
231
|
+
db.executemany(Sql.raw("INSERT INTO users (name) VALUES (?)"), users)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Run several statements at once:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
db.executescript(Sql.raw("""
|
|
238
|
+
DELETE FROM users WHERE id = 1;
|
|
239
|
+
UPDATE users SET name = 'Robert' WHERE name = 'Bob';
|
|
240
|
+
"""))
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Pragmas
|
|
244
|
+
|
|
245
|
+
SQLite's defaults are conservative, most notably **foreign keys are not enforced** unless you ask. `RECOMMENDED_PRAGMAS` turns on the settings most applications want: foreign keys, WAL journalling (readers do not block the writer) and `synchronous = normal`.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from sqlitey import Db, RECOMMENDED_PRAGMAS
|
|
249
|
+
|
|
250
|
+
with Db("app.db", pragmas=RECOMMENDED_PRAGMAS) as db:
|
|
251
|
+
...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Or choose your own:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
with Db("app.db", pragmas={"foreign_keys": True, "cache_size": -8000}) as db:
|
|
258
|
+
...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Migrations
|
|
262
|
+
|
|
263
|
+
Keep your schema changes as numbered `.sql` files, named `<version>_<description>.sql`:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
migrations/
|
|
267
|
+
001_create_users.sql
|
|
268
|
+
002_add_email.sql
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Then call `migrate` when your application starts:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
from sqlitey import Db
|
|
276
|
+
|
|
277
|
+
with Db("app.db") as db:
|
|
278
|
+
applied = db.migrate(Path("migrations"))
|
|
279
|
+
print(f"{applied} migration(s) applied")
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- Only files newer than the database's current version are run, in order. The version is stored in the database itself (`PRAGMA user_version`), so no extra table is needed.
|
|
283
|
+
- Each file is all-or-nothing. If one fails, the database stays at the previous version and the error is raised.
|
|
284
|
+
- It is safe to call from several processes at once, each file is applied only once.
|
|
285
|
+
|
|
286
|
+
To add a change later, add a new file with the next number. Never edit a migration that has already been applied.
|
|
287
|
+
|
|
288
|
+
## Things to know
|
|
289
|
+
|
|
290
|
+
- **Do not commit inside a `db.transaction()` block.** `db.commit()` and `db.executescript()` both end the transaction early, and the block then fails on exit.
|
|
291
|
+
- **`db.migrate()` needs a clean slate.** It raises `RuntimeError` if you have uncommitted changes, call it first.
|
|
292
|
+
- **Migration files must not contain `BEGIN` or `COMMIT`.** Each file already runs in its own transaction.
|
|
293
|
+
- **SQL files are cached.** A file is read once per process, restart to pick up edits.
|
|
294
|
+
- **Need something not covered here?** The underlying `sqlite3` connection is available as `db.conn`.
|
|
295
|
+
|
|
296
|
+
## API reference
|
|
297
|
+
|
|
298
|
+
### Sql
|
|
299
|
+
|
|
300
|
+
| Method | Description |
|
|
301
|
+
|--------|-------------|
|
|
302
|
+
| `Sql.raw(query)` | A query from an inline string |
|
|
303
|
+
| `Sql.template(filename, path=None)` | A query loaded from a `.sql` file |
|
|
304
|
+
|
|
305
|
+
### Db
|
|
306
|
+
|
|
307
|
+
| Method | Description |
|
|
308
|
+
|--------|-------------|
|
|
309
|
+
| `Db(database, row_factory=None, sql_templates_dir=None, autocommit=False, pragmas=None)` | Open a database. Extra arguments are passed to `sqlite3.connect()` |
|
|
310
|
+
| `Db.from_config(config, **kwargs)` | Open a database from a `DbPathConfig` |
|
|
311
|
+
| `execute(sql, params=())` | Run a query and return a cursor |
|
|
312
|
+
| `executemany(sql, seq_of_params)` | Run a query once per set of parameters |
|
|
313
|
+
| `executescript(sql)` | Run several statements as a script |
|
|
314
|
+
| `fetchone(sql, params=())` | Run a query and return the first row, or `None` |
|
|
315
|
+
| `fetchall(sql, params=())` | Run a query and return all rows |
|
|
316
|
+
| `fetchval(sql, params=())` | Run a query and return the first column of the first row, or `None` |
|
|
317
|
+
| `commit(sql, params=())` | Run a query and commit |
|
|
318
|
+
| `transaction()` | Context manager, runs a block atomically, nestable |
|
|
319
|
+
| `migrate(directory)` | Apply pending migration files, return how many ran |
|
|
320
|
+
| `close()` | Close the connection, discarding uncommitted changes |
|
|
321
|
+
| `conn` | The underlying `sqlite3.Connection` |
|
|
322
|
+
|
|
323
|
+
### Row factories
|
|
324
|
+
|
|
325
|
+
| Factory | Rows are returned as |
|
|
326
|
+
|---------|----------------------|
|
|
327
|
+
| *(none)* | Tuples |
|
|
328
|
+
| `dict_factory` | Dictionaries |
|
|
329
|
+
| `namedtuple_factory` | Namedtuples |
|
|
330
|
+
| `dataclass_factory(cls)` | Instances of `cls` |
|
|
331
|
+
|
|
332
|
+
### Other
|
|
333
|
+
|
|
334
|
+
| Name | Description |
|
|
335
|
+
|------|-------------|
|
|
336
|
+
| `DbPathConfig(database, sql_templates_dir=None)` | Bundles a database path and a SQL directory |
|
|
337
|
+
| `RECOMMENDED_PRAGMAS` | Foreign keys on, WAL journalling, `synchronous = normal` |
|
|
338
|
+
|
|
339
|
+
## Development
|
|
340
|
+
|
|
341
|
+
The project uses [uv](https://docs.astral.sh/uv/).
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
make deps # install dependencies
|
|
345
|
+
make tests # run the test suite
|
|
346
|
+
make ruff # lint
|
|
347
|
+
make fmt # format
|
|
348
|
+
make mypy # type check
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The [test suite](https://github.com/smallwat3r/sqlitey/blob/main/tests/test_sqlitey.py) doubles as a set of runnable examples.
|
|
352
|
+
|
|
353
|
+
## Licence
|
|
354
|
+
|
|
355
|
+
MIT, see [LICENSE](LICENSE).
|