sqlalchemyseed 2.1.0__tar.gz → 2.4.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.
- sqlalchemyseed-2.4.0/PKG-INFO +243 -0
- sqlalchemyseed-2.4.0/README.md +219 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/pyproject.toml +10 -3
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/__init__.py +1 -1
- sqlalchemyseed-2.4.0/src/sqlalchemyseed/__main__.py +6 -0
- sqlalchemyseed-2.4.0/src/sqlalchemyseed/cli.py +148 -0
- sqlalchemyseed-2.4.0/src/sqlalchemyseed/loader.py +87 -0
- sqlalchemyseed-2.4.0/src/sqlalchemyseed/pytest_plugin.py +66 -0
- sqlalchemyseed-2.4.0/src/sqlalchemyseed.egg-info/PKG-INFO +243 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/SOURCES.txt +6 -0
- sqlalchemyseed-2.4.0/src/sqlalchemyseed.egg-info/entry_points.txt +5 -0
- sqlalchemyseed-2.4.0/tests/test_cli.py +149 -0
- sqlalchemyseed-2.4.0/tests/test_loader.py +77 -0
- sqlalchemyseed-2.4.0/tests/test_pytest_plugin.py +331 -0
- sqlalchemyseed-2.1.0/PKG-INFO +0 -143
- sqlalchemyseed-2.1.0/README.md +0 -118
- sqlalchemyseed-2.1.0/src/sqlalchemyseed/loader.py +0 -64
- sqlalchemyseed-2.1.0/src/sqlalchemyseed.egg-info/PKG-INFO +0 -143
- sqlalchemyseed-2.1.0/tests/test_loader.py +0 -41
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/LICENSE +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/setup.cfg +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/__init__.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/seeder.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/attribute.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/constants.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/dynamic_seeder.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/errors.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/json.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/seeder.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/util.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/validator.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/dependency_links.txt +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/requires.txt +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/top_level.txt +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_json.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_seeder.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_temp_seeder.py +0 -0
- {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_validator.py +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemyseed
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Summary: SQLAlchemy Seeder
|
|
5
|
+
Author-email: Jedy Matt Tabasco <hello@jedymatt.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jedymatt/sqlalchemyseed
|
|
8
|
+
Project-URL: Documentation, https://sqlalchemyseed.readthedocs.io/
|
|
9
|
+
Project-URL: Source, https://github.com/jedymatt/sqlalchemyseed
|
|
10
|
+
Project-URL: Tracker, https://github.com/jedymatt/sqlalchemyseed/issues
|
|
11
|
+
Keywords: sqlalchemy,orm,seed,seeder,json,yaml
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: SQLAlchemy>=2.0
|
|
21
|
+
Provides-Extra: yaml
|
|
22
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# sqlalchemyseed
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/sqlalchemyseed)
|
|
28
|
+
[](https://pypi.org/project/sqlalchemyseed)
|
|
29
|
+
[](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
|
|
30
|
+
[](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)
|
|
31
|
+
[](https://codeclimate.com/github/jedymatt/sqlalchemyseed/maintainability)
|
|
32
|
+
[](https://codecov.io/gh/jedymatt/sqlalchemyseed)
|
|
33
|
+
[](https://sqlalchemyseed.readthedocs.io/en/latest/?badge=latest)
|
|
34
|
+
|
|
35
|
+
Sqlalchemy seeder that supports nested relationships.
|
|
36
|
+
|
|
37
|
+
Supported file types
|
|
38
|
+
|
|
39
|
+
- json
|
|
40
|
+
- yaml
|
|
41
|
+
- csv
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Default installation
|
|
46
|
+
|
|
47
|
+
```shell
|
|
48
|
+
pip install sqlalchemyseed
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
main.py
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from sqlalchemyseed import load_entities_from_json
|
|
57
|
+
from sqlalchemyseed import Seeder
|
|
58
|
+
from db import session
|
|
59
|
+
|
|
60
|
+
# load entities
|
|
61
|
+
entities = load_entities_from_json('data.json')
|
|
62
|
+
|
|
63
|
+
# Initializing Seeder
|
|
64
|
+
seeder = Seeder(session)
|
|
65
|
+
|
|
66
|
+
# Seeding
|
|
67
|
+
seeder.seed(entities)
|
|
68
|
+
|
|
69
|
+
# Committing
|
|
70
|
+
session.commit() # or seeder.session.commit()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
data.json
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"model": "models.Person",
|
|
78
|
+
"data": [
|
|
79
|
+
{
|
|
80
|
+
"name": "John March",
|
|
81
|
+
"age": 23
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "Juan Dela Cruz",
|
|
85
|
+
"age": 21
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Command-line usage
|
|
92
|
+
|
|
93
|
+
Seed a database directly from data files without writing Python:
|
|
94
|
+
|
|
95
|
+
```shell
|
|
96
|
+
sqlalchemyseed data.json --url sqlite:///app.db
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The command accepts one or more files and/or directories (a directory seeds
|
|
100
|
+
every `.json`/`.yaml`/`.yml` file inside it, in sorted order):
|
|
101
|
+
|
|
102
|
+
```shell
|
|
103
|
+
sqlalchemyseed seeds/ --url "$DATABASE_URL"
|
|
104
|
+
sqlalchemyseed a.json b.yaml --url sqlite:///app.db
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The database URL may be passed with `--url` or the `DATABASE_URL` environment
|
|
108
|
+
variable. Model paths in the data files (e.g. `models.Person`) are resolved
|
|
109
|
+
against the current working directory, so run the command from your project
|
|
110
|
+
root.
|
|
111
|
+
|
|
112
|
+
Options:
|
|
113
|
+
|
|
114
|
+
- `--dry-run` — seed inside a transaction, then roll back (validate without writing)
|
|
115
|
+
- `--seeder hybrid` — use `HybridSeeder` instead of the default `Seeder`
|
|
116
|
+
- `--model models.Person` — required for CSV inputs, which are not self-describing
|
|
117
|
+
- `--ref-prefix` — override the relationship reference prefix (default `!`)
|
|
118
|
+
|
|
119
|
+
The same command is available as a module:
|
|
120
|
+
|
|
121
|
+
```shell
|
|
122
|
+
python -m sqlalchemyseed data.json --url sqlite:///app.db
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Testing with pytest
|
|
126
|
+
|
|
127
|
+
Installing `sqlalchemyseed` alongside `pytest` registers a plugin that loads
|
|
128
|
+
fixture files into a transactionally-isolated session. Provide one `engine`
|
|
129
|
+
fixture in your `conftest.py`; the plugin supplies `sqlalchemyseed_session`
|
|
130
|
+
(rolled back after every test) and a `seed` factory.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# conftest.py
|
|
134
|
+
import pytest
|
|
135
|
+
from sqlalchemy import create_engine, event
|
|
136
|
+
from sqlalchemy.pool import StaticPool
|
|
137
|
+
|
|
138
|
+
from myapp.models import Base
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.fixture(scope="session")
|
|
142
|
+
def engine():
|
|
143
|
+
# StaticPool keeps a single in-memory connection alive so the schema you
|
|
144
|
+
# create is visible to the test session. A file-based or server database
|
|
145
|
+
# needs no such tweak — just return your usual engine.
|
|
146
|
+
engine = create_engine(
|
|
147
|
+
"sqlite://",
|
|
148
|
+
connect_args={"check_same_thread": False},
|
|
149
|
+
poolclass=StaticPool,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# SQLite only: hand transaction control to SQLAlchemy so an explicit
|
|
153
|
+
# commit() inside a test lands on a savepoint and is rolled back with the
|
|
154
|
+
# outer transaction. Left to itself the pysqlite driver commits straight to
|
|
155
|
+
# the database and the per-test rollback cannot undo it. Other databases
|
|
156
|
+
# (PostgreSQL, MySQL) need neither listener.
|
|
157
|
+
@event.listens_for(engine, "connect")
|
|
158
|
+
def _sqlite_no_driver_begin(dbapi_connection, connection_record):
|
|
159
|
+
dbapi_connection.isolation_level = None
|
|
160
|
+
|
|
161
|
+
@event.listens_for(engine, "begin")
|
|
162
|
+
def _sqlite_emit_begin(connection):
|
|
163
|
+
connection.exec_driver_sql("BEGIN")
|
|
164
|
+
|
|
165
|
+
Base.metadata.create_all(engine)
|
|
166
|
+
return engine
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# test_people.py
|
|
171
|
+
from myapp.models import Person
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_people_are_seeded(seed, sqlalchemyseed_session):
|
|
175
|
+
seeder = seed("tests/data/people.yaml")
|
|
176
|
+
assert sqlalchemyseed_session.query(Person).count() == 2
|
|
177
|
+
assert seeder.instances[0].name == "Alice"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`seed()` accepts the same inputs as the library: `.json`, `.yaml`/`.yml`, and
|
|
181
|
+
`.csv` files. CSV is not self-describing, so pass the model:
|
|
182
|
+
`seed("people.csv", model="myapp.models.Person")`. Use `seeder="hybrid"` for the
|
|
183
|
+
`HybridSeeder`, and `ref_prefix=...` to override the relationship reference
|
|
184
|
+
prefix. Every test runs inside a transaction that is rolled back afterward, so
|
|
185
|
+
tests never see each other's rows.
|
|
186
|
+
|
|
187
|
+
> **Note:** the plugin registers fixtures named `engine`, `sqlalchemyseed_session`,
|
|
188
|
+
> and `seed`. Defining your own `engine` fixture is how you plug in your database;
|
|
189
|
+
> if you already use those names for something else, your definitions take
|
|
190
|
+
> precedence (pytest resolves conftest fixtures over plugin fixtures).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
<https://sqlalchemyseed.readthedocs.io/>
|
|
195
|
+
|
|
196
|
+
## Found Bug?
|
|
197
|
+
|
|
198
|
+
Report here in this link:
|
|
199
|
+
<https://github.com/jedymatt/sqlalchemyseed/issues>
|
|
200
|
+
|
|
201
|
+
## Want to contribute?
|
|
202
|
+
|
|
203
|
+
First, Clone this [repository](https://github.com/jedymatt/sqlalchemyseed).
|
|
204
|
+
|
|
205
|
+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management and running tasks.
|
|
206
|
+
|
|
207
|
+
### Install dev dependencies
|
|
208
|
+
|
|
209
|
+
Inside the folder, sync the environment (uv creates the virtualenv and installs the project plus dev dependencies):
|
|
210
|
+
|
|
211
|
+
```shell
|
|
212
|
+
uv sync
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Run tests
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
uv run pytest
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Run the tests against a specific Python version (uv downloads it if needed):
|
|
222
|
+
|
|
223
|
+
```shell
|
|
224
|
+
uv run --python 3.14 pytest
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Run the tests against the lowest supported dependencies (e.g. SQLAlchemy 2.0):
|
|
228
|
+
|
|
229
|
+
```shell
|
|
230
|
+
uv run --resolution lowest-direct pytest
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Run tests with coverage:
|
|
234
|
+
|
|
235
|
+
```shell
|
|
236
|
+
uv run coverage run -m pytest
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Autobuild documentation
|
|
240
|
+
|
|
241
|
+
```shell
|
|
242
|
+
sphinx-autobuild docs docs/_build/html
|
|
243
|
+
```
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# sqlalchemyseed
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sqlalchemyseed)
|
|
4
|
+
[](https://pypi.org/project/sqlalchemyseed)
|
|
5
|
+
[](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)
|
|
7
|
+
[](https://codeclimate.com/github/jedymatt/sqlalchemyseed/maintainability)
|
|
8
|
+
[](https://codecov.io/gh/jedymatt/sqlalchemyseed)
|
|
9
|
+
[](https://sqlalchemyseed.readthedocs.io/en/latest/?badge=latest)
|
|
10
|
+
|
|
11
|
+
Sqlalchemy seeder that supports nested relationships.
|
|
12
|
+
|
|
13
|
+
Supported file types
|
|
14
|
+
|
|
15
|
+
- json
|
|
16
|
+
- yaml
|
|
17
|
+
- csv
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Default installation
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
pip install sqlalchemyseed
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
main.py
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from sqlalchemyseed import load_entities_from_json
|
|
33
|
+
from sqlalchemyseed import Seeder
|
|
34
|
+
from db import session
|
|
35
|
+
|
|
36
|
+
# load entities
|
|
37
|
+
entities = load_entities_from_json('data.json')
|
|
38
|
+
|
|
39
|
+
# Initializing Seeder
|
|
40
|
+
seeder = Seeder(session)
|
|
41
|
+
|
|
42
|
+
# Seeding
|
|
43
|
+
seeder.seed(entities)
|
|
44
|
+
|
|
45
|
+
# Committing
|
|
46
|
+
session.commit() # or seeder.session.commit()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
data.json
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"model": "models.Person",
|
|
54
|
+
"data": [
|
|
55
|
+
{
|
|
56
|
+
"name": "John March",
|
|
57
|
+
"age": 23
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "Juan Dela Cruz",
|
|
61
|
+
"age": 21
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Command-line usage
|
|
68
|
+
|
|
69
|
+
Seed a database directly from data files without writing Python:
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
sqlalchemyseed data.json --url sqlite:///app.db
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The command accepts one or more files and/or directories (a directory seeds
|
|
76
|
+
every `.json`/`.yaml`/`.yml` file inside it, in sorted order):
|
|
77
|
+
|
|
78
|
+
```shell
|
|
79
|
+
sqlalchemyseed seeds/ --url "$DATABASE_URL"
|
|
80
|
+
sqlalchemyseed a.json b.yaml --url sqlite:///app.db
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The database URL may be passed with `--url` or the `DATABASE_URL` environment
|
|
84
|
+
variable. Model paths in the data files (e.g. `models.Person`) are resolved
|
|
85
|
+
against the current working directory, so run the command from your project
|
|
86
|
+
root.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
|
|
90
|
+
- `--dry-run` — seed inside a transaction, then roll back (validate without writing)
|
|
91
|
+
- `--seeder hybrid` — use `HybridSeeder` instead of the default `Seeder`
|
|
92
|
+
- `--model models.Person` — required for CSV inputs, which are not self-describing
|
|
93
|
+
- `--ref-prefix` — override the relationship reference prefix (default `!`)
|
|
94
|
+
|
|
95
|
+
The same command is available as a module:
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
python -m sqlalchemyseed data.json --url sqlite:///app.db
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Testing with pytest
|
|
102
|
+
|
|
103
|
+
Installing `sqlalchemyseed` alongside `pytest` registers a plugin that loads
|
|
104
|
+
fixture files into a transactionally-isolated session. Provide one `engine`
|
|
105
|
+
fixture in your `conftest.py`; the plugin supplies `sqlalchemyseed_session`
|
|
106
|
+
(rolled back after every test) and a `seed` factory.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# conftest.py
|
|
110
|
+
import pytest
|
|
111
|
+
from sqlalchemy import create_engine, event
|
|
112
|
+
from sqlalchemy.pool import StaticPool
|
|
113
|
+
|
|
114
|
+
from myapp.models import Base
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.fixture(scope="session")
|
|
118
|
+
def engine():
|
|
119
|
+
# StaticPool keeps a single in-memory connection alive so the schema you
|
|
120
|
+
# create is visible to the test session. A file-based or server database
|
|
121
|
+
# needs no such tweak — just return your usual engine.
|
|
122
|
+
engine = create_engine(
|
|
123
|
+
"sqlite://",
|
|
124
|
+
connect_args={"check_same_thread": False},
|
|
125
|
+
poolclass=StaticPool,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# SQLite only: hand transaction control to SQLAlchemy so an explicit
|
|
129
|
+
# commit() inside a test lands on a savepoint and is rolled back with the
|
|
130
|
+
# outer transaction. Left to itself the pysqlite driver commits straight to
|
|
131
|
+
# the database and the per-test rollback cannot undo it. Other databases
|
|
132
|
+
# (PostgreSQL, MySQL) need neither listener.
|
|
133
|
+
@event.listens_for(engine, "connect")
|
|
134
|
+
def _sqlite_no_driver_begin(dbapi_connection, connection_record):
|
|
135
|
+
dbapi_connection.isolation_level = None
|
|
136
|
+
|
|
137
|
+
@event.listens_for(engine, "begin")
|
|
138
|
+
def _sqlite_emit_begin(connection):
|
|
139
|
+
connection.exec_driver_sql("BEGIN")
|
|
140
|
+
|
|
141
|
+
Base.metadata.create_all(engine)
|
|
142
|
+
return engine
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# test_people.py
|
|
147
|
+
from myapp.models import Person
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_people_are_seeded(seed, sqlalchemyseed_session):
|
|
151
|
+
seeder = seed("tests/data/people.yaml")
|
|
152
|
+
assert sqlalchemyseed_session.query(Person).count() == 2
|
|
153
|
+
assert seeder.instances[0].name == "Alice"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`seed()` accepts the same inputs as the library: `.json`, `.yaml`/`.yml`, and
|
|
157
|
+
`.csv` files. CSV is not self-describing, so pass the model:
|
|
158
|
+
`seed("people.csv", model="myapp.models.Person")`. Use `seeder="hybrid"` for the
|
|
159
|
+
`HybridSeeder`, and `ref_prefix=...` to override the relationship reference
|
|
160
|
+
prefix. Every test runs inside a transaction that is rolled back afterward, so
|
|
161
|
+
tests never see each other's rows.
|
|
162
|
+
|
|
163
|
+
> **Note:** the plugin registers fixtures named `engine`, `sqlalchemyseed_session`,
|
|
164
|
+
> and `seed`. Defining your own `engine` fixture is how you plug in your database;
|
|
165
|
+
> if you already use those names for something else, your definitions take
|
|
166
|
+
> precedence (pytest resolves conftest fixtures over plugin fixtures).
|
|
167
|
+
|
|
168
|
+
## Documentation
|
|
169
|
+
|
|
170
|
+
<https://sqlalchemyseed.readthedocs.io/>
|
|
171
|
+
|
|
172
|
+
## Found Bug?
|
|
173
|
+
|
|
174
|
+
Report here in this link:
|
|
175
|
+
<https://github.com/jedymatt/sqlalchemyseed/issues>
|
|
176
|
+
|
|
177
|
+
## Want to contribute?
|
|
178
|
+
|
|
179
|
+
First, Clone this [repository](https://github.com/jedymatt/sqlalchemyseed).
|
|
180
|
+
|
|
181
|
+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management and running tasks.
|
|
182
|
+
|
|
183
|
+
### Install dev dependencies
|
|
184
|
+
|
|
185
|
+
Inside the folder, sync the environment (uv creates the virtualenv and installs the project plus dev dependencies):
|
|
186
|
+
|
|
187
|
+
```shell
|
|
188
|
+
uv sync
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Run tests
|
|
192
|
+
|
|
193
|
+
```shell
|
|
194
|
+
uv run pytest
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Run the tests against a specific Python version (uv downloads it if needed):
|
|
198
|
+
|
|
199
|
+
```shell
|
|
200
|
+
uv run --python 3.14 pytest
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Run the tests against the lowest supported dependencies (e.g. SQLAlchemy 2.0):
|
|
204
|
+
|
|
205
|
+
```shell
|
|
206
|
+
uv run --resolution lowest-direct pytest
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Run tests with coverage:
|
|
210
|
+
|
|
211
|
+
```shell
|
|
212
|
+
uv run coverage run -m pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Autobuild documentation
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
sphinx-autobuild docs docs/_build/html
|
|
219
|
+
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "sqlalchemyseed"
|
|
3
3
|
description = "SQLAlchemy Seeder"
|
|
4
4
|
readme = "README.md"
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
authors = [
|
|
@@ -10,7 +10,6 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
keywords = ["sqlalchemy", "orm", "seed", "seeder", "json", "yaml"]
|
|
12
12
|
classifiers = [
|
|
13
|
-
"Programming Language :: Python :: 3.9",
|
|
14
13
|
"Programming Language :: Python :: 3.10",
|
|
15
14
|
"Programming Language :: Python :: 3.11",
|
|
16
15
|
"Programming Language :: Python :: 3.12",
|
|
@@ -27,6 +26,12 @@ yaml = [
|
|
|
27
26
|
"PyYAML>=6.0",
|
|
28
27
|
]
|
|
29
28
|
|
|
29
|
+
[project.scripts]
|
|
30
|
+
sqlalchemyseed = "sqlalchemyseed.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.entry-points.pytest11]
|
|
33
|
+
sqlalchemyseed = "sqlalchemyseed.pytest_plugin"
|
|
34
|
+
|
|
30
35
|
[project.urls]
|
|
31
36
|
Homepage = "https://github.com/jedymatt/sqlalchemyseed"
|
|
32
37
|
Documentation = "https://sqlalchemyseed.readthedocs.io/"
|
|
@@ -35,7 +40,9 @@ Tracker = "https://github.com/jedymatt/sqlalchemyseed/issues"
|
|
|
35
40
|
|
|
36
41
|
[dependency-groups]
|
|
37
42
|
dev = [
|
|
38
|
-
|
|
43
|
+
# 9.0.3 fixes GHSA-6w46-j5rx-g56g (tmpdir handling); it needs Python >=3.10,
|
|
44
|
+
# which is now the floor.
|
|
45
|
+
"pytest>=9.0.3",
|
|
39
46
|
"coverage>=6.2",
|
|
40
47
|
"PyYAML>=6.0",
|
|
41
48
|
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Command-line interface for seeding a database from data files."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import sqlalchemy
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from . import loader
|
|
12
|
+
from .seeder import HybridSeeder, Seeder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
"""Build the argument parser for the ``sqlalchemyseed`` command."""
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="sqlalchemyseed",
|
|
19
|
+
description="Seed a database from JSON, YAML, or CSV data files.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"paths",
|
|
23
|
+
nargs="+",
|
|
24
|
+
metavar="PATH",
|
|
25
|
+
help="data files or directories to seed from",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--url",
|
|
29
|
+
help="SQLAlchemy database URL (defaults to the DATABASE_URL env var)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--seeder",
|
|
33
|
+
choices=("basic", "hybrid"),
|
|
34
|
+
default="basic",
|
|
35
|
+
help="seeder to use (default: basic)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--model",
|
|
39
|
+
help="model class path (e.g. models.Person) required for CSV inputs",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--ref-prefix",
|
|
43
|
+
default="!",
|
|
44
|
+
help="prefix marking relationship references (default: !)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--dry-run",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="seed within a transaction but roll back instead of committing",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--debug",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="re-raise errors with a full traceback instead of a one-line message",
|
|
55
|
+
)
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def collect_files(paths) -> list:
|
|
60
|
+
"""Expand each path into data files, walking directories in sorted order."""
|
|
61
|
+
files = []
|
|
62
|
+
for raw_path in paths:
|
|
63
|
+
files.extend(_files_in(Path(raw_path)))
|
|
64
|
+
return files
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _files_in(path: Path) -> list:
|
|
68
|
+
"""Return the data files contributed by a single path argument."""
|
|
69
|
+
if path.is_dir():
|
|
70
|
+
return _discover_directory(path)
|
|
71
|
+
if path.is_file():
|
|
72
|
+
return [path]
|
|
73
|
+
raise FileNotFoundError(f"path does not exist: {path}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _discover_directory(directory: Path) -> list:
|
|
77
|
+
"""Return the JSON/YAML files inside a directory, sorted by name."""
|
|
78
|
+
discovered = sorted(
|
|
79
|
+
child for child in directory.iterdir()
|
|
80
|
+
if child.suffix.lower() in loader.DISCOVERABLE_EXTENSIONS
|
|
81
|
+
)
|
|
82
|
+
if not discovered:
|
|
83
|
+
raise FileNotFoundError(
|
|
84
|
+
f"no JSON or YAML seed files found in directory: {directory}"
|
|
85
|
+
)
|
|
86
|
+
return discovered
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _make_seeder(name, session, ref_prefix):
|
|
90
|
+
"""Return the seeder implementation selected on the command line."""
|
|
91
|
+
if name == "hybrid":
|
|
92
|
+
return HybridSeeder(session, ref_prefix=ref_prefix)
|
|
93
|
+
return Seeder(session, ref_prefix=ref_prefix)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _seed_all(seeder, files, model) -> int:
|
|
97
|
+
"""Seed every file through the seeder and return the entity count."""
|
|
98
|
+
seeded = 0
|
|
99
|
+
for path in files:
|
|
100
|
+
seeder.seed(loader.load_path(path, model))
|
|
101
|
+
seeded += len(seeder.instances)
|
|
102
|
+
return seeded
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv=None) -> int:
|
|
106
|
+
"""Entry point for the ``sqlalchemyseed`` command."""
|
|
107
|
+
parser = build_parser()
|
|
108
|
+
args = parser.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
url = args.url or os.environ.get("DATABASE_URL")
|
|
111
|
+
if not url:
|
|
112
|
+
parser.error("a database URL is required via --url or the DATABASE_URL env var")
|
|
113
|
+
|
|
114
|
+
# Make the caller's project importable so model paths like "models.Person"
|
|
115
|
+
# resolve against the current working directory.
|
|
116
|
+
sys.path.insert(0, os.getcwd())
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
files = collect_files(args.paths)
|
|
120
|
+
except FileNotFoundError as error:
|
|
121
|
+
parser.error(str(error))
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
engine = sqlalchemy.create_engine(url)
|
|
125
|
+
with Session(engine) as session:
|
|
126
|
+
seeder = _make_seeder(args.seeder, session, args.ref_prefix)
|
|
127
|
+
seeded = _seed_all(seeder, files, args.model)
|
|
128
|
+
return _finish(session, seeded, len(files), args.dry_run)
|
|
129
|
+
except Exception as error: # noqa: BLE001 - top-level boundary: report any failure as exit code 1
|
|
130
|
+
if args.debug:
|
|
131
|
+
raise
|
|
132
|
+
print(f"error: {type(error).__name__}: {error}", file=sys.stderr)
|
|
133
|
+
return 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _finish(session, seeded, file_count, dry_run) -> int:
|
|
137
|
+
"""Commit or roll back the seeded session and print a summary."""
|
|
138
|
+
if dry_run:
|
|
139
|
+
session.rollback()
|
|
140
|
+
print(f"Dry run: would seed {seeded} entities from {file_count} file(s) (rolled back).")
|
|
141
|
+
return 0
|
|
142
|
+
session.commit()
|
|
143
|
+
print(f"Seeded {seeded} entities from {file_count} file(s).")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__": # pragma: no cover
|
|
148
|
+
raise SystemExit(main())
|