dataclassdb 0.1.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.
- dataclassdb-0.1.0/LICENSE +21 -0
- dataclassdb-0.1.0/PKG-INFO +237 -0
- dataclassdb-0.1.0/README.md +214 -0
- dataclassdb-0.1.0/pyproject.toml +33 -0
- dataclassdb-0.1.0/setup.cfg +4 -0
- dataclassdb-0.1.0/src/dataclassdb/__init__.py +19 -0
- dataclassdb-0.1.0/src/dataclassdb/builders/function_builder.py +407 -0
- dataclassdb-0.1.0/src/dataclassdb/builders/list_utils.py +48 -0
- dataclassdb-0.1.0/src/dataclassdb/builders/query_builder.py +158 -0
- dataclassdb-0.1.0/src/dataclassdb/builders/statement_builder.py +635 -0
- dataclassdb-0.1.0/src/dataclassdb/builders/string_builder.py +98 -0
- dataclassdb-0.1.0/src/dataclassdb/constants.py +372 -0
- dataclassdb-0.1.0/src/dataclassdb/dataclass_db.py +282 -0
- dataclassdb-0.1.0/src/dataclassdb/dataclass_field_codec.py +216 -0
- dataclassdb-0.1.0/src/dataclassdb/dataclass_sqlite_field.py +193 -0
- dataclassdb-0.1.0/src/dataclassdb/dataclass_sqlite_table.py +152 -0
- dataclassdb-0.1.0/src/dataclassdb/dataclass_types.py +22 -0
- dataclassdb-0.1.0/src/dataclassdb/db_engine.py +126 -0
- dataclassdb-0.1.0/src/dataclassdb/utils.py +74 -0
- dataclassdb-0.1.0/src/dataclassdb.egg-info/PKG-INFO +237 -0
- dataclassdb-0.1.0/src/dataclassdb.egg-info/SOURCES.txt +35 -0
- dataclassdb-0.1.0/src/dataclassdb.egg-info/dependency_links.txt +1 -0
- dataclassdb-0.1.0/src/dataclassdb.egg-info/requires.txt +1 -0
- dataclassdb-0.1.0/src/dataclassdb.egg-info/top_level.txt +1 -0
- dataclassdb-0.1.0/tests/test_annotation_parsing.py +171 -0
- dataclassdb-0.1.0/tests/test_builders.py +25 -0
- dataclassdb-0.1.0/tests/test_custom_codec.py +40 -0
- dataclassdb-0.1.0/tests/test_dataclass_db.py +260 -0
- dataclassdb-0.1.0/tests/test_dataclass_field_codec.py +222 -0
- dataclassdb-0.1.0/tests/test_dataclass_sqlite_codec.py +115 -0
- dataclassdb-0.1.0/tests/test_date_codec.py +115 -0
- dataclassdb-0.1.0/tests/test_db_engine.py +87 -0
- dataclassdb-0.1.0/tests/test_list_utils.py +34 -0
- dataclassdb-0.1.0/tests/test_no_primary.py +41 -0
- dataclassdb-0.1.0/tests/test_query_builder.py +140 -0
- dataclassdb-0.1.0/tests/test_string_builder.py +93 -0
- dataclassdb-0.1.0/tests/test_utils.py +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stuartsartcode
|
|
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
|
+
SOFTWARE.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataclassdb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: This module provides a lightweight sqlite3 ORM using normal dataclass.
|
|
5
|
+
Author-email: "@stuarts-art" <stuartsartemail@gmail.com>
|
|
6
|
+
Maintainer-email: "@stuarts-art" <stuartsartemail@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Repository, https://github.com/stuarts-art/DataclassDb
|
|
9
|
+
Keywords: dataclasses,sqlite3,orm
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: dacite>=1.9.2
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# dataclassdb
|
|
25
|
+
|
|
26
|
+
This module provides a lightweight sqlite3 ORM for dataclasses.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
To install dataclassdb, simply use 'pip:
|
|
31
|
+
|
|
32
|
+
```zsh
|
|
33
|
+
pip install dataclassdb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
Minimum Python version supported by `dataclassdb` is 3.11. The only dependency is [dacite](https://github.com/konradhalas/dacite).
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclassdb import DataclassDb
|
|
44
|
+
from types import Annotated
|
|
45
|
+
from dataclasses import dataclass, Annotated
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Foo:
|
|
49
|
+
id: Annotated[int, "PRIMARY KEY"]
|
|
50
|
+
bar: str
|
|
51
|
+
baz: list[int]
|
|
52
|
+
|
|
53
|
+
with DataclassDb(Foo, "example.db") as db:
|
|
54
|
+
# Insert and get
|
|
55
|
+
db.insert(Foo(0, "x", [1, 2, 3]))
|
|
56
|
+
foo = db.get(0)
|
|
57
|
+
|
|
58
|
+
# Arbitrary SQL
|
|
59
|
+
db.SELECT("bar").FROM(FOO).WHERE("id").eq("?")
|
|
60
|
+
selection = db.execute_one(0) # Replaces ? with 0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Overview
|
|
64
|
+
|
|
65
|
+
- Uses `@dataclass` classes to generate and interact with database files using `sqlite3`. No "Model" super class required.
|
|
66
|
+
- SQL table columns are inferred by the dataclass field annotation.
|
|
67
|
+
- Codecs(Encoder/Decoder) translate Python to SQL and back.
|
|
68
|
+
- Build in [QueryBuilder](src/dataclassdb/builders/query_builder.py) with every SQL statement and function for easy query writing
|
|
69
|
+
- Built in [DbEngine](src/dataclassdb/db_engine.py) context manager for managing the db connection, creating tables, and executing queries.
|
|
70
|
+
|
|
71
|
+
## Deeper Dive
|
|
72
|
+
|
|
73
|
+
- Each `@dataclass` fields' SQL type is inferred by the python type or by override. By default, undefined mapping are set to "TEXT".
|
|
74
|
+
- A Codec (Encoder/Decoder) is used to translate between python and SQL. The following are supported:
|
|
75
|
+
- **Basic Types**: `int`, `str`, `float`, `bool`
|
|
76
|
+
- **Enums**: `Enum`, `StrEnum`
|
|
77
|
+
- **Dates**: `datetime -> INTEGER (unix time)`, `datetime -> TEXT (ISO 8601)`
|
|
78
|
+
- **Json**: `list`, `dict`, `@dataclass` (If all elements are json encodable)
|
|
79
|
+
- **Pickle (Bytes)**: Any `python` object. Notably `set`s are not json encodable. Pickle is not safe and should be used with caution**.
|
|
80
|
+
- Any user defined [Codec](src/dataclassdb/dataclass_types.py#8)
|
|
81
|
+
- [Constraints](https://www.sqlite.org/syntax/column-constraint.html)
|
|
82
|
+
such as `PRIMARY KEY` or `UNIQUE` can be added as `typing.Annotated` metadata without affecting the base type.
|
|
83
|
+
- Dataclass field defaults are encoded
|
|
84
|
+
- "NOT NULL" field status is inferred by the
|
|
85
|
+
|
|
86
|
+
Column type is inferred by the python type and SQL type.
|
|
87
|
+
|
|
88
|
+
- The column type is inferred by the python type but can be overridden as the first annotation.
|
|
89
|
+
- This package does not attempt to abstract away SQL behavior. It's simply a lightweight wrapper around sqlite3.
|
|
90
|
+
|
|
91
|
+
## Motivation
|
|
92
|
+
|
|
93
|
+
### 1. Easier to write than plaintext
|
|
94
|
+
|
|
95
|
+
- All sqlite [keywords](src/dataclassdb/sql_statement_builder.py) and [functions](src/dataclassdb/sql_function_builder.py) are supported
|
|
96
|
+
- Queries can be written written without filler words.
|
|
97
|
+
- Lists and args are handled how you'd expect them to be handled.
|
|
98
|
+
|
|
99
|
+
### 2. Easier to write than [SqlAlchemy](https://docs.sqlalchemy.org/), [SqlModel](https://sqlmodel.tiangolo.com/features/#editor-support), and other more complex abstractions
|
|
100
|
+
|
|
101
|
+
- No required imports [QueryBuilder](src/dataclassdb/query_builder.py)
|
|
102
|
+
- Minimal package specific syntax or abstractions to learn
|
|
103
|
+
|
|
104
|
+
### 3. Abstracts away Non-SQL, `sqlite3`, actions
|
|
105
|
+
|
|
106
|
+
- Creating connection
|
|
107
|
+
- Executing the query
|
|
108
|
+
- Returning the result as a list or dictionary
|
|
109
|
+
|
|
110
|
+
## Features
|
|
111
|
+
|
|
112
|
+
- [DataclassDb](#dataclassdb): Maps dataclass class into a sqlite3 table.
|
|
113
|
+
- [QueryBuilder](#query-builder): Specialized StringBuilder with methods for each sqlite3 [keywords](src/dataclassdb/sql_statement_builder.py) and [functions](src/dataclassdb/sql_function_builder.py). Note that DataclassDb inherits from QueryBuilder.
|
|
114
|
+
- [DbEngine](src/dataclassdb/db_engine.py):
|
|
115
|
+
- [Codec](src/dataclassdb/dataclass_types.py#5): Protocol
|
|
116
|
+
- [CustomCodec](src/dataclassdb/dataclass_types.py#12)
|
|
117
|
+
|
|
118
|
+
## DataclassDb
|
|
119
|
+
|
|
120
|
+
### Column Definition
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
col_name: Annotated(Type, ? [SQL Type], *[Constraints], [Codec])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Type: The python type for the dataclass field
|
|
127
|
+
- SQL Type: Override the default mapped SQL type (see next section)
|
|
128
|
+
- Constraints: SQL constraints as text
|
|
129
|
+
- Codec: Overrides the default codec. Any object that is a [Codec](src/dataclassdb/dataclass_types.py#5) (has an encode and decode method)
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
|
|
133
|
+
| Python Field | Sqlite Col Definition |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `id: Annotated[int, "PRIMARY KEY"]` | `id INTEGER PRIMARY KEY NOT NULL` |
|
|
136
|
+
| `username: Annotated[str, "UNIQUE"]` | `username TEXT UNIQUE NOT NULL` |
|
|
137
|
+
| `email: str = ""` | `email TEXT NOT NULL DEFAULT ""` |
|
|
138
|
+
|
|
139
|
+
### Python type to SQL type resolution
|
|
140
|
+
|
|
141
|
+
Because sqlite has a limited amount of supported types: (INTEGER, TEXT, REAL, NUMERICAL, BLOB), we must map between python types and SQL types.
|
|
142
|
+
|
|
143
|
+
The "easiest" way to do this is by converting our python object into binary using pickle and storing it as a BLOB. However, there are a few issues with this approach.
|
|
144
|
+
|
|
145
|
+
1. Pickle is unsafe (bad actors can run arbitrary code on decode.)
|
|
146
|
+
2. This data cannot be read without decoding.
|
|
147
|
+
3. This data cannot be used in queries without decoding.
|
|
148
|
+
|
|
149
|
+
This package approaches this issue by using Codecs (Encoder/Decoder) to handle the python -> sqlite -> python conversions.
|
|
150
|
+
If no SQL type is declared in the Annotated column definition, first checks the below mapping.
|
|
151
|
+
|
|
152
|
+
If no mapping is found, the column is assumed to be json encodable. This setting can be overridden by either providing a custom Codec or setting the type to "BLOB".
|
|
153
|
+
|
|
154
|
+
| SQL Type | python type | Codec |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| TEXT | datetime | DatetimeTextCodec |
|
|
157
|
+
| TEXT | str | IdentityCodec |
|
|
158
|
+
| TEXT | default | JsonCodec |
|
|
159
|
+
| INTEGER | datetime | DatetimeIntCodec |
|
|
160
|
+
| INTEGER | bool | BoolCodec |
|
|
161
|
+
| INTEGER | int | IdentityCodec |
|
|
162
|
+
| REAL | float | IdentityCodec |
|
|
163
|
+
| BLOB | @dataclass | DataclassPickleCodec |
|
|
164
|
+
| BLOB | default | PickleCodec |
|
|
165
|
+
|
|
166
|
+
## Query Builder
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
qb = QueryBuilder() # str(qb)
|
|
170
|
+
qb.SELECT # = SELECT
|
|
171
|
+
qb("a", "b").Group_concat("c") # = SELECT a b group_concat(c)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- All SQL statement keywords, such as `SELECT`, `FROM`, etc. are available as **chainable** properties of Query builders `query.SELECT` or as standalone variables `db.SELECT`. See [StatementBuilder](src/dataclassdb/statement_builder.py) or [SQL enum](src/dataclassdb/constants.py#L4)
|
|
175
|
+
- Similarly, all SQL functions, such as `count`, `sum`, `group_concat` are available as **chainable** functions `query.SELECT.Count("type")` or as standalone functions `db.Count("type")`. See [QueryBuilder](src/dataclassdb/query_builder.py) or [SQL_FUNC enum](src/dataclassdb/constants.py#L161)
|
|
176
|
+
- QueryBuilders `__call__` adds provided arguments to the string as a list, with optional quote and parenthesis options.
|
|
177
|
+
|
|
178
|
+
QueryBuilder is a fancy SQL flavored String Builder which the following goals:
|
|
179
|
+
|
|
180
|
+
### Convention breaking weirdness
|
|
181
|
+
|
|
182
|
+
SQL [statements](src/dataclassdb/sql_statement_builder.py) leverage a totally legal but questionable hack. Each statement is a [`@property`](https://docs.python.org/3/library/functions.html#property) that returns self. The
|
|
183
|
+
[`__call__`](https://docs.python.org/3/reference/datamodel.html#emulating-callable-objects) handles the case where the object is called directly. Note that SQL functions are **NOT** properties.
|
|
184
|
+
|
|
185
|
+
So to for the Query `qb.SELECT("a","b")`
|
|
186
|
+
|
|
187
|
+
1. `qb.SELECT`: Calls the property `SELECT` which adds `"SELECT"` to the string. It returns self.
|
|
188
|
+
2. `qb.SELECT("a", "b")` calls `__call__` function instead of `SELECT` function.
|
|
189
|
+
|
|
190
|
+
### Comparing QueryBuilder, plain sqlite3 and sqlAlchemy
|
|
191
|
+
|
|
192
|
+
#### QueryBuilder
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
query = QueryBuilder("example.db").
|
|
196
|
+
query.SELECT("name").FROM("sqlite_master").WHERE("name='spam'")
|
|
197
|
+
result = query.execute()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Sqlite3
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
#sqlite3
|
|
204
|
+
con = sqlite3.connect("example.db")
|
|
205
|
+
cur = con.cursor()
|
|
206
|
+
res = cur.execute("SELECT name FROM sqlite_master WHERE name='spam'")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### sqlAlchemy
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
engine = create_engine("sqlite:///example.db")
|
|
213
|
+
with engine.connect() as conn:
|
|
214
|
+
query = text("SELECT name FROM sqlite_master WHERE name='spam'")
|
|
215
|
+
res = conn.execute(query)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Bonus functions
|
|
219
|
+
|
|
220
|
+
- `placeholders(n)` adds string "(? ... n)"
|
|
221
|
+
- `show()` prints the query at its current state
|
|
222
|
+
- `br` newline
|
|
223
|
+
- `lpar` "\("
|
|
224
|
+
- `rpar` "\)"
|
|
225
|
+
- `rpar` "\)"
|
|
226
|
+
- `comma` ","
|
|
227
|
+
- `execute(*args)`: Executes the command with the provided args replacing the placeholders.
|
|
228
|
+
|
|
229
|
+
## Created By
|
|
230
|
+
|
|
231
|
+
Stuart (@stuarts_art)
|
|
232
|
+
|
|
233
|
+
- Washed dev who now makes furry art
|
|
234
|
+
- I branched this out of [ArtRefSync](https://github.com/stuarts-art/ArtRefSync), a pure python desktop ui to sync reference art from SFW and NSFW image boards.
|
|
235
|
+
- It uses client provided api keys to ethically get data while respecting usage rules
|
|
236
|
+
- User defined black list to avoid the scourge of AI art
|
|
237
|
+
- Is not using excessive ram to spy on you (plus memory is expensive)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# dataclassdb
|
|
2
|
+
|
|
3
|
+
This module provides a lightweight sqlite3 ORM for dataclasses.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install dataclassdb, simply use 'pip:
|
|
8
|
+
|
|
9
|
+
```zsh
|
|
10
|
+
pip install dataclassdb
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
Minimum Python version supported by `dataclassdb` is 3.11. The only dependency is [dacite](https://github.com/konradhalas/dacite).
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from dataclassdb import DataclassDb
|
|
21
|
+
from types import Annotated
|
|
22
|
+
from dataclasses import dataclass, Annotated
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Foo:
|
|
26
|
+
id: Annotated[int, "PRIMARY KEY"]
|
|
27
|
+
bar: str
|
|
28
|
+
baz: list[int]
|
|
29
|
+
|
|
30
|
+
with DataclassDb(Foo, "example.db") as db:
|
|
31
|
+
# Insert and get
|
|
32
|
+
db.insert(Foo(0, "x", [1, 2, 3]))
|
|
33
|
+
foo = db.get(0)
|
|
34
|
+
|
|
35
|
+
# Arbitrary SQL
|
|
36
|
+
db.SELECT("bar").FROM(FOO).WHERE("id").eq("?")
|
|
37
|
+
selection = db.execute_one(0) # Replaces ? with 0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
|
|
42
|
+
- Uses `@dataclass` classes to generate and interact with database files using `sqlite3`. No "Model" super class required.
|
|
43
|
+
- SQL table columns are inferred by the dataclass field annotation.
|
|
44
|
+
- Codecs(Encoder/Decoder) translate Python to SQL and back.
|
|
45
|
+
- Build in [QueryBuilder](src/dataclassdb/builders/query_builder.py) with every SQL statement and function for easy query writing
|
|
46
|
+
- Built in [DbEngine](src/dataclassdb/db_engine.py) context manager for managing the db connection, creating tables, and executing queries.
|
|
47
|
+
|
|
48
|
+
## Deeper Dive
|
|
49
|
+
|
|
50
|
+
- Each `@dataclass` fields' SQL type is inferred by the python type or by override. By default, undefined mapping are set to "TEXT".
|
|
51
|
+
- A Codec (Encoder/Decoder) is used to translate between python and SQL. The following are supported:
|
|
52
|
+
- **Basic Types**: `int`, `str`, `float`, `bool`
|
|
53
|
+
- **Enums**: `Enum`, `StrEnum`
|
|
54
|
+
- **Dates**: `datetime -> INTEGER (unix time)`, `datetime -> TEXT (ISO 8601)`
|
|
55
|
+
- **Json**: `list`, `dict`, `@dataclass` (If all elements are json encodable)
|
|
56
|
+
- **Pickle (Bytes)**: Any `python` object. Notably `set`s are not json encodable. Pickle is not safe and should be used with caution**.
|
|
57
|
+
- Any user defined [Codec](src/dataclassdb/dataclass_types.py#8)
|
|
58
|
+
- [Constraints](https://www.sqlite.org/syntax/column-constraint.html)
|
|
59
|
+
such as `PRIMARY KEY` or `UNIQUE` can be added as `typing.Annotated` metadata without affecting the base type.
|
|
60
|
+
- Dataclass field defaults are encoded
|
|
61
|
+
- "NOT NULL" field status is inferred by the
|
|
62
|
+
|
|
63
|
+
Column type is inferred by the python type and SQL type.
|
|
64
|
+
|
|
65
|
+
- The column type is inferred by the python type but can be overridden as the first annotation.
|
|
66
|
+
- This package does not attempt to abstract away SQL behavior. It's simply a lightweight wrapper around sqlite3.
|
|
67
|
+
|
|
68
|
+
## Motivation
|
|
69
|
+
|
|
70
|
+
### 1. Easier to write than plaintext
|
|
71
|
+
|
|
72
|
+
- All sqlite [keywords](src/dataclassdb/sql_statement_builder.py) and [functions](src/dataclassdb/sql_function_builder.py) are supported
|
|
73
|
+
- Queries can be written written without filler words.
|
|
74
|
+
- Lists and args are handled how you'd expect them to be handled.
|
|
75
|
+
|
|
76
|
+
### 2. Easier to write than [SqlAlchemy](https://docs.sqlalchemy.org/), [SqlModel](https://sqlmodel.tiangolo.com/features/#editor-support), and other more complex abstractions
|
|
77
|
+
|
|
78
|
+
- No required imports [QueryBuilder](src/dataclassdb/query_builder.py)
|
|
79
|
+
- Minimal package specific syntax or abstractions to learn
|
|
80
|
+
|
|
81
|
+
### 3. Abstracts away Non-SQL, `sqlite3`, actions
|
|
82
|
+
|
|
83
|
+
- Creating connection
|
|
84
|
+
- Executing the query
|
|
85
|
+
- Returning the result as a list or dictionary
|
|
86
|
+
|
|
87
|
+
## Features
|
|
88
|
+
|
|
89
|
+
- [DataclassDb](#dataclassdb): Maps dataclass class into a sqlite3 table.
|
|
90
|
+
- [QueryBuilder](#query-builder): Specialized StringBuilder with methods for each sqlite3 [keywords](src/dataclassdb/sql_statement_builder.py) and [functions](src/dataclassdb/sql_function_builder.py). Note that DataclassDb inherits from QueryBuilder.
|
|
91
|
+
- [DbEngine](src/dataclassdb/db_engine.py):
|
|
92
|
+
- [Codec](src/dataclassdb/dataclass_types.py#5): Protocol
|
|
93
|
+
- [CustomCodec](src/dataclassdb/dataclass_types.py#12)
|
|
94
|
+
|
|
95
|
+
## DataclassDb
|
|
96
|
+
|
|
97
|
+
### Column Definition
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
col_name: Annotated(Type, ? [SQL Type], *[Constraints], [Codec])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- Type: The python type for the dataclass field
|
|
104
|
+
- SQL Type: Override the default mapped SQL type (see next section)
|
|
105
|
+
- Constraints: SQL constraints as text
|
|
106
|
+
- Codec: Overrides the default codec. Any object that is a [Codec](src/dataclassdb/dataclass_types.py#5) (has an encode and decode method)
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
|
|
110
|
+
| Python Field | Sqlite Col Definition |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `id: Annotated[int, "PRIMARY KEY"]` | `id INTEGER PRIMARY KEY NOT NULL` |
|
|
113
|
+
| `username: Annotated[str, "UNIQUE"]` | `username TEXT UNIQUE NOT NULL` |
|
|
114
|
+
| `email: str = ""` | `email TEXT NOT NULL DEFAULT ""` |
|
|
115
|
+
|
|
116
|
+
### Python type to SQL type resolution
|
|
117
|
+
|
|
118
|
+
Because sqlite has a limited amount of supported types: (INTEGER, TEXT, REAL, NUMERICAL, BLOB), we must map between python types and SQL types.
|
|
119
|
+
|
|
120
|
+
The "easiest" way to do this is by converting our python object into binary using pickle and storing it as a BLOB. However, there are a few issues with this approach.
|
|
121
|
+
|
|
122
|
+
1. Pickle is unsafe (bad actors can run arbitrary code on decode.)
|
|
123
|
+
2. This data cannot be read without decoding.
|
|
124
|
+
3. This data cannot be used in queries without decoding.
|
|
125
|
+
|
|
126
|
+
This package approaches this issue by using Codecs (Encoder/Decoder) to handle the python -> sqlite -> python conversions.
|
|
127
|
+
If no SQL type is declared in the Annotated column definition, first checks the below mapping.
|
|
128
|
+
|
|
129
|
+
If no mapping is found, the column is assumed to be json encodable. This setting can be overridden by either providing a custom Codec or setting the type to "BLOB".
|
|
130
|
+
|
|
131
|
+
| SQL Type | python type | Codec |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| TEXT | datetime | DatetimeTextCodec |
|
|
134
|
+
| TEXT | str | IdentityCodec |
|
|
135
|
+
| TEXT | default | JsonCodec |
|
|
136
|
+
| INTEGER | datetime | DatetimeIntCodec |
|
|
137
|
+
| INTEGER | bool | BoolCodec |
|
|
138
|
+
| INTEGER | int | IdentityCodec |
|
|
139
|
+
| REAL | float | IdentityCodec |
|
|
140
|
+
| BLOB | @dataclass | DataclassPickleCodec |
|
|
141
|
+
| BLOB | default | PickleCodec |
|
|
142
|
+
|
|
143
|
+
## Query Builder
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
qb = QueryBuilder() # str(qb)
|
|
147
|
+
qb.SELECT # = SELECT
|
|
148
|
+
qb("a", "b").Group_concat("c") # = SELECT a b group_concat(c)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- All SQL statement keywords, such as `SELECT`, `FROM`, etc. are available as **chainable** properties of Query builders `query.SELECT` or as standalone variables `db.SELECT`. See [StatementBuilder](src/dataclassdb/statement_builder.py) or [SQL enum](src/dataclassdb/constants.py#L4)
|
|
152
|
+
- Similarly, all SQL functions, such as `count`, `sum`, `group_concat` are available as **chainable** functions `query.SELECT.Count("type")` or as standalone functions `db.Count("type")`. See [QueryBuilder](src/dataclassdb/query_builder.py) or [SQL_FUNC enum](src/dataclassdb/constants.py#L161)
|
|
153
|
+
- QueryBuilders `__call__` adds provided arguments to the string as a list, with optional quote and parenthesis options.
|
|
154
|
+
|
|
155
|
+
QueryBuilder is a fancy SQL flavored String Builder which the following goals:
|
|
156
|
+
|
|
157
|
+
### Convention breaking weirdness
|
|
158
|
+
|
|
159
|
+
SQL [statements](src/dataclassdb/sql_statement_builder.py) leverage a totally legal but questionable hack. Each statement is a [`@property`](https://docs.python.org/3/library/functions.html#property) that returns self. The
|
|
160
|
+
[`__call__`](https://docs.python.org/3/reference/datamodel.html#emulating-callable-objects) handles the case where the object is called directly. Note that SQL functions are **NOT** properties.
|
|
161
|
+
|
|
162
|
+
So to for the Query `qb.SELECT("a","b")`
|
|
163
|
+
|
|
164
|
+
1. `qb.SELECT`: Calls the property `SELECT` which adds `"SELECT"` to the string. It returns self.
|
|
165
|
+
2. `qb.SELECT("a", "b")` calls `__call__` function instead of `SELECT` function.
|
|
166
|
+
|
|
167
|
+
### Comparing QueryBuilder, plain sqlite3 and sqlAlchemy
|
|
168
|
+
|
|
169
|
+
#### QueryBuilder
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
query = QueryBuilder("example.db").
|
|
173
|
+
query.SELECT("name").FROM("sqlite_master").WHERE("name='spam'")
|
|
174
|
+
result = query.execute()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Sqlite3
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
#sqlite3
|
|
181
|
+
con = sqlite3.connect("example.db")
|
|
182
|
+
cur = con.cursor()
|
|
183
|
+
res = cur.execute("SELECT name FROM sqlite_master WHERE name='spam'")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### sqlAlchemy
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
engine = create_engine("sqlite:///example.db")
|
|
190
|
+
with engine.connect() as conn:
|
|
191
|
+
query = text("SELECT name FROM sqlite_master WHERE name='spam'")
|
|
192
|
+
res = conn.execute(query)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Bonus functions
|
|
196
|
+
|
|
197
|
+
- `placeholders(n)` adds string "(? ... n)"
|
|
198
|
+
- `show()` prints the query at its current state
|
|
199
|
+
- `br` newline
|
|
200
|
+
- `lpar` "\("
|
|
201
|
+
- `rpar` "\)"
|
|
202
|
+
- `rpar` "\)"
|
|
203
|
+
- `comma` ","
|
|
204
|
+
- `execute(*args)`: Executes the command with the provided args replacing the placeholders.
|
|
205
|
+
|
|
206
|
+
## Created By
|
|
207
|
+
|
|
208
|
+
Stuart (@stuarts_art)
|
|
209
|
+
|
|
210
|
+
- Washed dev who now makes furry art
|
|
211
|
+
- I branched this out of [ArtRefSync](https://github.com/stuarts-art/ArtRefSync), a pure python desktop ui to sync reference art from SFW and NSFW image boards.
|
|
212
|
+
- It uses client provided api keys to ethically get data while respecting usage rules
|
|
213
|
+
- User defined black list to avoid the scourge of AI art
|
|
214
|
+
- Is not using excessive ram to spy on you (plus memory is expensive)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dataclassdb"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "This module provides a lightweight sqlite3 ORM using normal dataclass."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = ["dacite>=1.9.2"]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
authors = [{ name = "@stuarts-art", email = "stuartsartemail@gmail.com" }]
|
|
10
|
+
maintainers = [{ name = "@stuarts-art", email = "stuartsartemail@gmail.com" }]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Database :: Front-Ends",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["dataclasses", "sqlite3", "orm"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/stuarts-art/DataclassDb"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=9.1.1", "pytest-cov>=7.1.0", "ruff>=0.15.20"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = ["src"]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclassdb.dataclass_db import DataclassDb as DataclassDb
|
|
2
|
+
from dataclassdb.db_engine import DbEngine as DbEngine
|
|
3
|
+
from dataclassdb.builders.query_builder import QueryBuilder as QueryBuilder
|
|
4
|
+
from dataclassdb.builders.string_builder import StringBuilder as StringBuilder
|
|
5
|
+
from dataclassdb.dataclass_types import Codec as Codec
|
|
6
|
+
from dataclassdb.dataclass_types import CustomCodec as CustomCodec
|
|
7
|
+
from dataclassdb.dataclass_types import IsDataclass as IsDataclass
|
|
8
|
+
from dataclassdb.constants import SQL as SQL
|
|
9
|
+
from dataclassdb.constants import SQL_FUNC as SQL_FUNC
|
|
10
|
+
from dataclassdb.constants import SQL_FUNC_PRAGMA as SQL_FUNC_PRAGMA
|
|
11
|
+
from dataclassdb.constants import SQL_TYPES as SQL_TYPES
|
|
12
|
+
from dataclassdb.dataclass_sqlite_table import (
|
|
13
|
+
DataclassTableCodec as DataclassTableCodec,
|
|
14
|
+
)
|
|
15
|
+
from dataclassdb.dataclass_sqlite_table import decode_dict as decode_dict
|
|
16
|
+
from dataclassdb.dataclass_sqlite_table import decode_field as decode_field
|
|
17
|
+
from dataclassdb.dataclass_sqlite_table import encode_field as encode_field
|
|
18
|
+
from dataclassdb.dataclass_sqlite_table import get_class_codec as get_class_codec
|
|
19
|
+
from dataclassdb.dataclass_sqlite_table import get_field_codec as get_field_codec
|