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.
Files changed (38) hide show
  1. sqlalchemyseed-2.4.0/PKG-INFO +243 -0
  2. sqlalchemyseed-2.4.0/README.md +219 -0
  3. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/pyproject.toml +10 -3
  4. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/__init__.py +1 -1
  5. sqlalchemyseed-2.4.0/src/sqlalchemyseed/__main__.py +6 -0
  6. sqlalchemyseed-2.4.0/src/sqlalchemyseed/cli.py +148 -0
  7. sqlalchemyseed-2.4.0/src/sqlalchemyseed/loader.py +87 -0
  8. sqlalchemyseed-2.4.0/src/sqlalchemyseed/pytest_plugin.py +66 -0
  9. sqlalchemyseed-2.4.0/src/sqlalchemyseed.egg-info/PKG-INFO +243 -0
  10. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/SOURCES.txt +6 -0
  11. sqlalchemyseed-2.4.0/src/sqlalchemyseed.egg-info/entry_points.txt +5 -0
  12. sqlalchemyseed-2.4.0/tests/test_cli.py +149 -0
  13. sqlalchemyseed-2.4.0/tests/test_loader.py +77 -0
  14. sqlalchemyseed-2.4.0/tests/test_pytest_plugin.py +331 -0
  15. sqlalchemyseed-2.1.0/PKG-INFO +0 -143
  16. sqlalchemyseed-2.1.0/README.md +0 -118
  17. sqlalchemyseed-2.1.0/src/sqlalchemyseed/loader.py +0 -64
  18. sqlalchemyseed-2.1.0/src/sqlalchemyseed.egg-info/PKG-INFO +0 -143
  19. sqlalchemyseed-2.1.0/tests/test_loader.py +0 -41
  20. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/LICENSE +0 -0
  21. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/setup.cfg +0 -0
  22. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/__init__.py +0 -0
  23. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/seeder.py +0 -0
  24. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/attribute.py +0 -0
  25. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/constants.py +0 -0
  26. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/dynamic_seeder.py +0 -0
  27. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/errors.py +0 -0
  28. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/json.py +0 -0
  29. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/seeder.py +0 -0
  30. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/util.py +0 -0
  31. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/validator.py +0 -0
  32. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/dependency_links.txt +0 -0
  33. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/requires.txt +0 -0
  34. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/top_level.txt +0 -0
  35. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_json.py +0 -0
  36. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_seeder.py +0 -0
  37. {sqlalchemyseed-2.1.0 → sqlalchemyseed-2.4.0}/tests/test_temp_seeder.py +0 -0
  38. {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
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemyseed)](https://pypi.org/project/sqlalchemyseed)
28
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqlalchemyseed)](https://pypi.org/project/sqlalchemyseed)
29
+ [![PyPI - License](https://img.shields.io/pypi/l/sqlalchemyseed)](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
30
+ [![Python package](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml/badge.svg)](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)
31
+ [![Maintainability](https://api.codeclimate.com/v1/badges/2ca97c98929b614658ea/maintainability)](https://codeclimate.com/github/jedymatt/sqlalchemyseed/maintainability)
32
+ [![codecov](https://codecov.io/gh/jedymatt/sqlalchemyseed/branch/main/graph/badge.svg?token=W03MFZ2FAG)](https://codecov.io/gh/jedymatt/sqlalchemyseed)
33
+ [![Documentation Status](https://readthedocs.org/projects/sqlalchemyseed/badge/?version=latest)](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
+ [![PyPI](https://img.shields.io/pypi/v/sqlalchemyseed)](https://pypi.org/project/sqlalchemyseed)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqlalchemyseed)](https://pypi.org/project/sqlalchemyseed)
5
+ [![PyPI - License](https://img.shields.io/pypi/l/sqlalchemyseed)](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
6
+ [![Python package](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml/badge.svg)](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)
7
+ [![Maintainability](https://api.codeclimate.com/v1/badges/2ca97c98929b614658ea/maintainability)](https://codeclimate.com/github/jedymatt/sqlalchemyseed/maintainability)
8
+ [![codecov](https://codecov.io/gh/jedymatt/sqlalchemyseed/branch/main/graph/badge.svg?token=W03MFZ2FAG)](https://codecov.io/gh/jedymatt/sqlalchemyseed)
9
+ [![Documentation Status](https://readthedocs.org/projects/sqlalchemyseed/badge/?version=latest)](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.9"
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
- "pytest>=7.0",
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
  ]
@@ -11,7 +11,7 @@ from . import util
11
11
  from . import attribute
12
12
 
13
13
 
14
- __version__ = "2.1.0"
14
+ __version__ = "2.4.0"
15
15
 
16
16
  if __name__ == '__main__':
17
17
  pass
@@ -0,0 +1,6 @@
1
+ """Enable ``python -m sqlalchemyseed``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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())