deev 0.0.1__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.
- deev-0.0.1/LICENSE +21 -0
- deev-0.0.1/PKG-INFO +211 -0
- deev-0.0.1/README.md +187 -0
- deev-0.0.1/pyproject.toml +37 -0
- deev-0.0.1/setup.cfg +4 -0
- deev-0.0.1/src/deev/_ImmutableMixin.py +28 -0
- deev-0.0.1/src/deev/_MigrationData.py +20 -0
- deev-0.0.1/src/deev/__init__.py +30 -0
- deev-0.0.1/src/deev/common/ConnectionString.py +108 -0
- deev-0.0.1/src/deev/common/DbConnection.py +49 -0
- deev-0.0.1/src/deev/common/DbContext.py +12 -0
- deev-0.0.1/src/deev/common/DbCursor.py +66 -0
- deev-0.0.1/src/deev/common/DbError.py +19 -0
- deev-0.0.1/src/deev/common/DbMigrator.py +132 -0
- deev-0.0.1/src/deev/common/DbParams.py +9 -0
- deev-0.0.1/src/deev/common/DbTableAdapter.py +77 -0
- deev-0.0.1/src/deev/common/DbTransactionContext.py +83 -0
- deev-0.0.1/src/deev/common/DbTypeMapper.py +17 -0
- deev-0.0.1/src/deev/common/__init__.py +27 -0
- deev-0.0.1/src/deev/db_migrate.py +116 -0
- deev-0.0.1/src/deev/entities.py +284 -0
- deev-0.0.1/src/deev/mysql/MysqlTableAdapter.py +290 -0
- deev-0.0.1/src/deev/mysql/MysqlTransactionContext.py +169 -0
- deev-0.0.1/src/deev/mysql/MysqlTypeMapper.py +74 -0
- deev-0.0.1/src/deev/mysql/__init__.py +13 -0
- deev-0.0.1/src/deev/py.typed +0 -0
- deev-0.0.1/src/deev/sqlite/SqliteTableAdapter.py +290 -0
- deev-0.0.1/src/deev/sqlite/SqliteTransactionContext.py +170 -0
- deev-0.0.1/src/deev/sqlite/SqliteTypeMapper.py +69 -0
- deev-0.0.1/src/deev/sqlite/__init__.py +13 -0
- deev-0.0.1/src/deev/translation.py +309 -0
- deev-0.0.1/src/deev/utils.py +134 -0
- deev-0.0.1/src/deev/validation.py +87 -0
- deev-0.0.1/src/deev.egg-info/PKG-INFO +211 -0
- deev-0.0.1/src/deev.egg-info/SOURCES.txt +37 -0
- deev-0.0.1/src/deev.egg-info/dependency_links.txt +1 -0
- deev-0.0.1/src/deev.egg-info/entry_points.txt +2 -0
- deev-0.0.1/src/deev.egg-info/requires.txt +11 -0
- deev-0.0.1/src/deev.egg-info/top_level.txt +1 -0
deev-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
© 2023 Shaun Wilson
|
|
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 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.
|
deev-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deev
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: ..an entity framework for Python.
|
|
5
|
+
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: DB,database,entity framework,mapper
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: hanaro>=1.0.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: coverage; extra == "dev"
|
|
17
|
+
Requires-Dist: mypy; extra == "dev"
|
|
18
|
+
Requires-Dist: punit>=1.4.5; extra == "dev"
|
|
19
|
+
Requires-Dist: py-conventional-semver>=1.0.5; extra == "dev"
|
|
20
|
+
Requires-Dist: twine; extra == "dev"
|
|
21
|
+
Provides-Extra: mysql
|
|
22
|
+
Requires-Dist: mysql-connector-python; extra == "mysql"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/deev/) [](https://deev.readthedocs.io)
|
|
27
|
+
|
|
28
|
+
**deev** (דיב) is an entity framework for Python.
|
|
29
|
+
|
|
30
|
+
This README is only a high-level introduction to **deev**. For more detailed documentation, please view the official docs at [https://deev.readthedocs.io](https://deev.readthedocs.io).
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
* Entity-based; perform CRUD operations using Python objects instead of hand-crafting SQL.
|
|
36
|
+
* Validation; Entities validate before they get persisted to a database, also validate entities on-demand.
|
|
37
|
+
* Transaction Contexts; enter and exit transaction scopes with language-level context management, avoid mismanaged transaction states.
|
|
38
|
+
* DB Migrations; use Python code to apply (and undo) schema changes, data translation, etc using `db-migrate` CLI tool for use from CI/CD pipelines.
|
|
39
|
+
* PEP 249 compatible abstractions; no need to refactor code just to switch DBMS.
|
|
40
|
+
* Syntax normalization; parameterize SQL using `%?` instead of provider-specific syntaxes.
|
|
41
|
+
* Raw SQL Access; execute raw SQL as-needed, including provider/DBMS-specific functions (primarily intended for advanced `db-migrate` cases.)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
You can install `deev` from [PyPI](https://pypi.org/project/deev/) through usual means, such as `pip`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install deev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Let's have a look at the two popular use cases: using Python objects for CRUD operations, and using the `db-migrate` CLI tool to manage DB schema.
|
|
56
|
+
|
|
57
|
+
### Entity CRUD
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# imports
|
|
61
|
+
from deev import entity, field
|
|
62
|
+
|
|
63
|
+
# define a simple entity with an auto-increment PK, an int value column, and a list[str] column
|
|
64
|
+
@entity
|
|
65
|
+
class SimpleEntity:
|
|
66
|
+
id: int = field(autoincrement=True, primary_key=True)
|
|
67
|
+
column1: int
|
|
68
|
+
column2: list[str]
|
|
69
|
+
|
|
70
|
+
# create a database using familiar connection-string syntax
|
|
71
|
+
from deev.utils import create_database
|
|
72
|
+
|
|
73
|
+
connection_str = 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
74
|
+
create_database(connection_str)
|
|
75
|
+
|
|
76
|
+
# connect to your database, create a table for storage, and perform some CRUD operations
|
|
77
|
+
from deev import connect
|
|
78
|
+
from deev.sqlite import SqliteTableAdapter
|
|
79
|
+
with connect(connection_str) as db:
|
|
80
|
+
table = SqliteTableAdapter[SimpleEntity](db)
|
|
81
|
+
table.create_table()
|
|
82
|
+
# CREATE
|
|
83
|
+
entity_key = table.create(SimpleEntity(
|
|
84
|
+
column1=1,
|
|
85
|
+
column2=[3, 2, 1]
|
|
86
|
+
))
|
|
87
|
+
# READ
|
|
88
|
+
entity = table.read(**entity_key)
|
|
89
|
+
assert entity.id is not None
|
|
90
|
+
assert entity.column1 == 1
|
|
91
|
+
assert entity.column2[0] == 3
|
|
92
|
+
assert entity.column2[1] == 2
|
|
93
|
+
assert entity.column2[2] == 1
|
|
94
|
+
# UPDATE
|
|
95
|
+
entity.column2[1] = 4
|
|
96
|
+
table.update(entity)
|
|
97
|
+
# DELETE
|
|
98
|
+
table.delete(**entity_key)
|
|
99
|
+
|
|
100
|
+
# alternatives: upsert + query
|
|
101
|
+
entity_key = table.upsert(SimpleEntity(
|
|
102
|
+
column1=2,
|
|
103
|
+
column2=[5]
|
|
104
|
+
))
|
|
105
|
+
entity_key = table.upsert(SimpleEntity(
|
|
106
|
+
column1=2,
|
|
107
|
+
column2=[6]
|
|
108
|
+
))
|
|
109
|
+
results = table.query(
|
|
110
|
+
where='column1 = %?',
|
|
111
|
+
orderby='column1 DESC',
|
|
112
|
+
limit=2,
|
|
113
|
+
params=(2,)
|
|
114
|
+
)
|
|
115
|
+
count = 0
|
|
116
|
+
for result in results:
|
|
117
|
+
assert result.column2[0] in (5, 6)
|
|
118
|
+
count += 1
|
|
119
|
+
assert count == 2
|
|
120
|
+
# query kwargs are optional, for example this creates a generator for all table records:
|
|
121
|
+
results = table.query()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### CLI `db-migrate` Tool
|
|
125
|
+
|
|
126
|
+
The `db-migrate` tool can be used to apply a migration script or undo a previously applied migration script.
|
|
127
|
+
|
|
128
|
+
Basic syntax:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
$ db-migrate -h
|
|
132
|
+
usage: db-migrate [-h] [--verbose] <COMMAND> ...
|
|
133
|
+
|
|
134
|
+
Utility for applying, undoing, or generating migrations.
|
|
135
|
+
|
|
136
|
+
positional arguments:
|
|
137
|
+
<COMMAND> Action to perform.
|
|
138
|
+
apply Apply migrations.
|
|
139
|
+
undo Undo migrations.
|
|
140
|
+
|
|
141
|
+
options:
|
|
142
|
+
-h, --help show this help message and exit
|
|
143
|
+
--verbose Enable verbose logging.
|
|
144
|
+
|
|
145
|
+
$ db-migrate apply -h
|
|
146
|
+
usage: db-migrate apply [-h] [--stop-at name] path connectionstring
|
|
147
|
+
|
|
148
|
+
positional arguments:
|
|
149
|
+
path Directory containing migration scripts.
|
|
150
|
+
connectionstring Database connection string.
|
|
151
|
+
|
|
152
|
+
options:
|
|
153
|
+
-h, --help show this help message and exit
|
|
154
|
+
--stop-at name Stop processing at the named migration.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A migration script is a Python file which defines two functions `apply(...)` and `undo(...)`, each receiving a `DbTransactionContext` you can use to modify the database transactionally. As an example let's assume we modified `SimpleEntity` with an additional attribute `column3` of type `datetime`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
@entity
|
|
161
|
+
class SimpleEntity:
|
|
162
|
+
id: int = field(autoincrement=True, primary_key=True)
|
|
163
|
+
column1: int
|
|
164
|
+
column2: list[str]
|
|
165
|
+
column3: Optional[datetime]] = field(nullable=True)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Since we already have a table for this entity, we want to modify the schema to support the new attribute:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# 000_test01.py
|
|
172
|
+
from deev.common import DbTransactionContext
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def apply(transaction: DbTransactionContext) -> None:
|
|
176
|
+
# alter the existing entity table
|
|
177
|
+
transaction.execute_nonquery('ALTER TABLE SimpleEntity ADD COLUMN column3 DATETIME')
|
|
178
|
+
transaction.commit()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def undo(transaction: DbTransactionContext) -> None:
|
|
182
|
+
# undo the alteration applied by `apply(...)` above
|
|
183
|
+
transaction.execute_nonquery('ALTER TABLE SimpleEntity DROP COLUMN column3')
|
|
184
|
+
transaction.commit()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Finally, we can apply the change to our existing database:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# apply schema change
|
|
191
|
+
db-migrate apply ./test_data/migrations 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
192
|
+
```
|
|
193
|
+
```
|
|
194
|
+
..apply migration "000_test01"
|
|
195
|
+
Migrations applied 1, skipped 0, available 1.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
We can also undo the change after it has been applied:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# undo schema change
|
|
202
|
+
db-migrate apply ./test_data/migrations 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
203
|
+
```
|
|
204
|
+
```
|
|
205
|
+
..apply migration "000_test01"
|
|
206
|
+
Migrations undone 1, skipped 0, available 1.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Contact
|
|
210
|
+
|
|
211
|
+
You can reach me on [Discord](https://discordapp.com/users/307684202080501761) or [open an Issue on Github](https://github.com/wilson0x4d/deev/issues/new/choose).
|
deev-0.0.1/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
|
|
2
|
+
[](https://pypi.org/project/deev/) [](https://deev.readthedocs.io)
|
|
3
|
+
|
|
4
|
+
**deev** (דיב) is an entity framework for Python.
|
|
5
|
+
|
|
6
|
+
This README is only a high-level introduction to **deev**. For more detailed documentation, please view the official docs at [https://deev.readthedocs.io](https://deev.readthedocs.io).
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* Entity-based; perform CRUD operations using Python objects instead of hand-crafting SQL.
|
|
12
|
+
* Validation; Entities validate before they get persisted to a database, also validate entities on-demand.
|
|
13
|
+
* Transaction Contexts; enter and exit transaction scopes with language-level context management, avoid mismanaged transaction states.
|
|
14
|
+
* DB Migrations; use Python code to apply (and undo) schema changes, data translation, etc using `db-migrate` CLI tool for use from CI/CD pipelines.
|
|
15
|
+
* PEP 249 compatible abstractions; no need to refactor code just to switch DBMS.
|
|
16
|
+
* Syntax normalization; parameterize SQL using `%?` instead of provider-specific syntaxes.
|
|
17
|
+
* Raw SQL Access; execute raw SQL as-needed, including provider/DBMS-specific functions (primarily intended for advanced `db-migrate` cases.)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
You can install `deev` from [PyPI](https://pypi.org/project/deev/) through usual means, such as `pip`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install deev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Let's have a look at the two popular use cases: using Python objects for CRUD operations, and using the `db-migrate` CLI tool to manage DB schema.
|
|
32
|
+
|
|
33
|
+
### Entity CRUD
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# imports
|
|
37
|
+
from deev import entity, field
|
|
38
|
+
|
|
39
|
+
# define a simple entity with an auto-increment PK, an int value column, and a list[str] column
|
|
40
|
+
@entity
|
|
41
|
+
class SimpleEntity:
|
|
42
|
+
id: int = field(autoincrement=True, primary_key=True)
|
|
43
|
+
column1: int
|
|
44
|
+
column2: list[str]
|
|
45
|
+
|
|
46
|
+
# create a database using familiar connection-string syntax
|
|
47
|
+
from deev.utils import create_database
|
|
48
|
+
|
|
49
|
+
connection_str = 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
50
|
+
create_database(connection_str)
|
|
51
|
+
|
|
52
|
+
# connect to your database, create a table for storage, and perform some CRUD operations
|
|
53
|
+
from deev import connect
|
|
54
|
+
from deev.sqlite import SqliteTableAdapter
|
|
55
|
+
with connect(connection_str) as db:
|
|
56
|
+
table = SqliteTableAdapter[SimpleEntity](db)
|
|
57
|
+
table.create_table()
|
|
58
|
+
# CREATE
|
|
59
|
+
entity_key = table.create(SimpleEntity(
|
|
60
|
+
column1=1,
|
|
61
|
+
column2=[3, 2, 1]
|
|
62
|
+
))
|
|
63
|
+
# READ
|
|
64
|
+
entity = table.read(**entity_key)
|
|
65
|
+
assert entity.id is not None
|
|
66
|
+
assert entity.column1 == 1
|
|
67
|
+
assert entity.column2[0] == 3
|
|
68
|
+
assert entity.column2[1] == 2
|
|
69
|
+
assert entity.column2[2] == 1
|
|
70
|
+
# UPDATE
|
|
71
|
+
entity.column2[1] = 4
|
|
72
|
+
table.update(entity)
|
|
73
|
+
# DELETE
|
|
74
|
+
table.delete(**entity_key)
|
|
75
|
+
|
|
76
|
+
# alternatives: upsert + query
|
|
77
|
+
entity_key = table.upsert(SimpleEntity(
|
|
78
|
+
column1=2,
|
|
79
|
+
column2=[5]
|
|
80
|
+
))
|
|
81
|
+
entity_key = table.upsert(SimpleEntity(
|
|
82
|
+
column1=2,
|
|
83
|
+
column2=[6]
|
|
84
|
+
))
|
|
85
|
+
results = table.query(
|
|
86
|
+
where='column1 = %?',
|
|
87
|
+
orderby='column1 DESC',
|
|
88
|
+
limit=2,
|
|
89
|
+
params=(2,)
|
|
90
|
+
)
|
|
91
|
+
count = 0
|
|
92
|
+
for result in results:
|
|
93
|
+
assert result.column2[0] in (5, 6)
|
|
94
|
+
count += 1
|
|
95
|
+
assert count == 2
|
|
96
|
+
# query kwargs are optional, for example this creates a generator for all table records:
|
|
97
|
+
results = table.query()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### CLI `db-migrate` Tool
|
|
101
|
+
|
|
102
|
+
The `db-migrate` tool can be used to apply a migration script or undo a previously applied migration script.
|
|
103
|
+
|
|
104
|
+
Basic syntax:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ db-migrate -h
|
|
108
|
+
usage: db-migrate [-h] [--verbose] <COMMAND> ...
|
|
109
|
+
|
|
110
|
+
Utility for applying, undoing, or generating migrations.
|
|
111
|
+
|
|
112
|
+
positional arguments:
|
|
113
|
+
<COMMAND> Action to perform.
|
|
114
|
+
apply Apply migrations.
|
|
115
|
+
undo Undo migrations.
|
|
116
|
+
|
|
117
|
+
options:
|
|
118
|
+
-h, --help show this help message and exit
|
|
119
|
+
--verbose Enable verbose logging.
|
|
120
|
+
|
|
121
|
+
$ db-migrate apply -h
|
|
122
|
+
usage: db-migrate apply [-h] [--stop-at name] path connectionstring
|
|
123
|
+
|
|
124
|
+
positional arguments:
|
|
125
|
+
path Directory containing migration scripts.
|
|
126
|
+
connectionstring Database connection string.
|
|
127
|
+
|
|
128
|
+
options:
|
|
129
|
+
-h, --help show this help message and exit
|
|
130
|
+
--stop-at name Stop processing at the named migration.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A migration script is a Python file which defines two functions `apply(...)` and `undo(...)`, each receiving a `DbTransactionContext` you can use to modify the database transactionally. As an example let's assume we modified `SimpleEntity` with an additional attribute `column3` of type `datetime`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@entity
|
|
137
|
+
class SimpleEntity:
|
|
138
|
+
id: int = field(autoincrement=True, primary_key=True)
|
|
139
|
+
column1: int
|
|
140
|
+
column2: list[str]
|
|
141
|
+
column3: Optional[datetime]] = field(nullable=True)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Since we already have a table for this entity, we want to modify the schema to support the new attribute:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# 000_test01.py
|
|
148
|
+
from deev.common import DbTransactionContext
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def apply(transaction: DbTransactionContext) -> None:
|
|
152
|
+
# alter the existing entity table
|
|
153
|
+
transaction.execute_nonquery('ALTER TABLE SimpleEntity ADD COLUMN column3 DATETIME')
|
|
154
|
+
transaction.commit()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def undo(transaction: DbTransactionContext) -> None:
|
|
158
|
+
# undo the alteration applied by `apply(...)` above
|
|
159
|
+
transaction.execute_nonquery('ALTER TABLE SimpleEntity DROP COLUMN column3')
|
|
160
|
+
transaction.commit()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Finally, we can apply the change to our existing database:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# apply schema change
|
|
167
|
+
db-migrate apply ./test_data/migrations 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
168
|
+
```
|
|
169
|
+
```
|
|
170
|
+
..apply migration "000_test01"
|
|
171
|
+
Migrations applied 1, skipped 0, available 1.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
We can also undo the change after it has been applied:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# undo schema change
|
|
178
|
+
db-migrate apply ./test_data/migrations 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
|
|
179
|
+
```
|
|
180
|
+
```
|
|
181
|
+
..apply migration "000_test01"
|
|
182
|
+
Migrations undone 1, skipped 0, available 1.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Contact
|
|
186
|
+
|
|
187
|
+
You can reach me on [Discord](https://discordapp.com/users/307684202080501761) or [open an Issue on Github](https://github.com/wilson0x4d/deev/issues/new/choose).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "deev"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "..an entity framework for Python."
|
|
5
|
+
keywords = ["DB", "database", "entity framework", "mapper"]
|
|
6
|
+
authors = [
|
|
7
|
+
{ name="Shaun Wilson", email="mrshaunwilson@msn.com" }
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python"
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"hanaro (>=1.0.0)"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"coverage",
|
|
24
|
+
"mypy",
|
|
25
|
+
"punit (>=1.4.5)",
|
|
26
|
+
"py-conventional-semver (>=1.0.5)",
|
|
27
|
+
"twine"
|
|
28
|
+
]
|
|
29
|
+
mysql = [
|
|
30
|
+
"mysql-connector-python"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
db-migrate = "deev.db_migrate:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
"deev" = ["py.typed"]
|
deev-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _ImmutableMixin:
|
|
8
|
+
"""Mixin that adds a ``freeze`` operation."""
|
|
9
|
+
__frozen__: bool = False
|
|
10
|
+
|
|
11
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
12
|
+
if getattr(self, '__frozen__', False):
|
|
13
|
+
raise AttributeError(f'Cannot modify frozen instance: {name}')
|
|
14
|
+
# Call ``super()`` so the next class in the MRO (e.g. BaseModel) can run
|
|
15
|
+
super().__setattr__(name, value)
|
|
16
|
+
|
|
17
|
+
def __delattr__(self, name: str) -> None:
|
|
18
|
+
if getattr(self, '__frozen__', False):
|
|
19
|
+
raise AttributeError(f'Cannot delete attribute from frozen instance: {name}')
|
|
20
|
+
super().__delattr__(name)
|
|
21
|
+
|
|
22
|
+
def __freeze__(self) -> Any:
|
|
23
|
+
"""Mark the instance as immutable."""
|
|
24
|
+
object.__setattr__(self, '__frozen__', True)
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ['_ImmutableMixin']
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2023 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .entities import entity, field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@entity(table_name='_migrationdata')
|
|
8
|
+
class _MigrationData:
|
|
9
|
+
"""Internal entity representation of ``_migrationdata`` tables used by ``deev``."""
|
|
10
|
+
id: int = field(
|
|
11
|
+
autoincrement=True,
|
|
12
|
+
primary_key=True
|
|
13
|
+
)
|
|
14
|
+
migration: str = field(
|
|
15
|
+
max=260,
|
|
16
|
+
sqltype='VARCVHAR(260)'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ['_MigrationData']
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2023 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .common.ConnectionString import ConnectionString
|
|
5
|
+
from .common.DbError import DbError
|
|
6
|
+
from .entities import (
|
|
7
|
+
entity,
|
|
8
|
+
field
|
|
9
|
+
)
|
|
10
|
+
from .utils import connect
|
|
11
|
+
from . import common, entities, mysql, translation, utils, validation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__version__ = '0.0.1'
|
|
15
|
+
__commit__ = 'cb1b8ad'
|
|
16
|
+
__all__ = [
|
|
17
|
+
'__version__', '__commit__',
|
|
18
|
+
'ConnectionString',
|
|
19
|
+
'DbError',
|
|
20
|
+
'common',
|
|
21
|
+
'connect',
|
|
22
|
+
'db_migrate',
|
|
23
|
+
'entities',
|
|
24
|
+
'entity',
|
|
25
|
+
'field',
|
|
26
|
+
'mysql',
|
|
27
|
+
'translation',
|
|
28
|
+
'utils',
|
|
29
|
+
'validation'
|
|
30
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2023 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConnectionString:
|
|
10
|
+
"""
|
|
11
|
+
A type-safe "Connection String" representation that can parse/build constituent parts.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__server: Optional[str]
|
|
15
|
+
__database: Optional[str]
|
|
16
|
+
__user: Optional[str]
|
|
17
|
+
__password: Optional[str]
|
|
18
|
+
__provider: Optional[str]
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
connection_str: Optional[str] = None
|
|
23
|
+
):
|
|
24
|
+
self.__server = None
|
|
25
|
+
self.__database = None
|
|
26
|
+
self.__user = None
|
|
27
|
+
self.__password = None
|
|
28
|
+
self.__provider = None
|
|
29
|
+
if connection_str is not None:
|
|
30
|
+
self.parse(connection_str)
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
parts = []
|
|
34
|
+
if self.server is not None:
|
|
35
|
+
parts.append(f'Server={self.server}')
|
|
36
|
+
if self.database is not None:
|
|
37
|
+
parts.append(f'Database={self.database}')
|
|
38
|
+
if self.user is not None:
|
|
39
|
+
parts.append(f'UID={self.user}')
|
|
40
|
+
if self.password is not None:
|
|
41
|
+
parts.append(f'PWD={self.password}')
|
|
42
|
+
if self.provider is not None:
|
|
43
|
+
parts.append(f'Provider={self.provider}')
|
|
44
|
+
return ';'.join(parts)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def server(self) -> Optional[str]:
|
|
48
|
+
return self.__server
|
|
49
|
+
|
|
50
|
+
@server.setter
|
|
51
|
+
def server(self, value: Optional[str]):
|
|
52
|
+
self.__server = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def database(self) -> Optional[str]:
|
|
56
|
+
return self.__database
|
|
57
|
+
|
|
58
|
+
@database.setter
|
|
59
|
+
def database(self, value: Optional[str]):
|
|
60
|
+
self.__database = value
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def user(self) -> Optional[str]:
|
|
64
|
+
return self.__user
|
|
65
|
+
|
|
66
|
+
@user.setter
|
|
67
|
+
def user(self, value: Optional[str]):
|
|
68
|
+
self.__user = value
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def password(self) -> Optional[str]:
|
|
72
|
+
return self.__password
|
|
73
|
+
|
|
74
|
+
@password.setter
|
|
75
|
+
def password(self, value: Optional[str]):
|
|
76
|
+
self.__password = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def provider(self) -> Optional[str]:
|
|
80
|
+
return self.__provider
|
|
81
|
+
|
|
82
|
+
@provider.setter
|
|
83
|
+
def provider(self, value: Optional[str]):
|
|
84
|
+
self.__provider = value
|
|
85
|
+
|
|
86
|
+
def parse(self, connectionstring: Optional[str]) -> ConnectionString:
|
|
87
|
+
parts = (
|
|
88
|
+
[]
|
|
89
|
+
if connectionstring is None
|
|
90
|
+
else connectionstring.split(';')
|
|
91
|
+
)
|
|
92
|
+
for part in parts:
|
|
93
|
+
key, value = part.split('=')
|
|
94
|
+
match key.lower():
|
|
95
|
+
case 'server':
|
|
96
|
+
self.server = value
|
|
97
|
+
case 'database':
|
|
98
|
+
self.database = value
|
|
99
|
+
case 'uid' | 'user' | 'user id' | 'username':
|
|
100
|
+
self.user = value
|
|
101
|
+
case 'pwd' | 'password' | 'pass':
|
|
102
|
+
self.password = value
|
|
103
|
+
case 'provider':
|
|
104
|
+
self.provider = value
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = ['ConnectionString']
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2023 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Literal, Optional, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from .DbCursor import DbCursor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class DbConnection(Protocol):
|
|
14
|
+
"""DB-API 2.0 Connection proto."""
|
|
15
|
+
|
|
16
|
+
def cursor(self, *args: Any, **kwargs: Any) -> DbCursor:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def commit(self) -> None:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def rollback(self) -> None:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def close(self) -> None:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def __enter__(self) -> DbConnection:
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def __exit__(self, exc_type: Optional[type[BaseException]], exc: Optional[BaseException], tb: Optional[TracebackType], /) -> Literal[False]:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def __subclasshook__(cls, subclass: type) -> bool | None: # type: ignore[override]
|
|
36
|
+
# if db api providers can't be bothered to follow the
|
|
37
|
+
# spec anyone else can't be bothered to enforce it.
|
|
38
|
+
required = {
|
|
39
|
+
name
|
|
40
|
+
for name in dir(cls)
|
|
41
|
+
if not name.startswith('_')
|
|
42
|
+
}
|
|
43
|
+
for name in required:
|
|
44
|
+
if name not in subclass.__dict__:
|
|
45
|
+
return False # pragma: no cover
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ['DbConnection']
|