sqlalchemyseed 2.2.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 (35) hide show
  1. {sqlalchemyseed-2.2.0/src/sqlalchemyseed.egg-info → sqlalchemyseed-2.4.0}/PKG-INFO +69 -3
  2. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/README.md +67 -0
  3. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/pyproject.toml +7 -3
  4. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/__init__.py +1 -1
  5. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/cli.py +17 -38
  6. sqlalchemyseed-2.4.0/src/sqlalchemyseed/loader.py +87 -0
  7. sqlalchemyseed-2.4.0/src/sqlalchemyseed/pytest_plugin.py +66 -0
  8. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0/src/sqlalchemyseed.egg-info}/PKG-INFO +69 -3
  9. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/SOURCES.txt +2 -0
  10. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/entry_points.txt +3 -0
  11. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/tests/test_cli.py +17 -1
  12. sqlalchemyseed-2.4.0/tests/test_loader.py +77 -0
  13. sqlalchemyseed-2.4.0/tests/test_pytest_plugin.py +331 -0
  14. sqlalchemyseed-2.2.0/src/sqlalchemyseed/loader.py +0 -64
  15. sqlalchemyseed-2.2.0/tests/test_loader.py +0 -41
  16. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/LICENSE +0 -0
  17. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/setup.cfg +0 -0
  18. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/__main__.py +0 -0
  19. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/__init__.py +0 -0
  20. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/_future/seeder.py +0 -0
  21. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/attribute.py +0 -0
  22. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/constants.py +0 -0
  23. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/dynamic_seeder.py +0 -0
  24. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/errors.py +0 -0
  25. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/json.py +0 -0
  26. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/seeder.py +0 -0
  27. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/util.py +0 -0
  28. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed/validator.py +0 -0
  29. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/dependency_links.txt +0 -0
  30. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/requires.txt +0 -0
  31. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/src/sqlalchemyseed.egg-info/top_level.txt +0 -0
  32. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/tests/test_json.py +0 -0
  33. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/tests/test_seeder.py +0 -0
  34. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/tests/test_temp_seeder.py +0 -0
  35. {sqlalchemyseed-2.2.0 → sqlalchemyseed-2.4.0}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemyseed
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: SQLAlchemy Seeder
5
5
  Author-email: Jedy Matt Tabasco <hello@jedymatt.dev>
6
6
  License-Expression: MIT
@@ -9,13 +9,12 @@ Project-URL: Documentation, https://sqlalchemyseed.readthedocs.io/
9
9
  Project-URL: Source, https://github.com/jedymatt/sqlalchemyseed
10
10
  Project-URL: Tracker, https://github.com/jedymatt/sqlalchemyseed/issues
11
11
  Keywords: sqlalchemy,orm,seed,seeder,json,yaml
12
- Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
16
  Classifier: Programming Language :: Python :: 3.14
18
- Requires-Python: >=3.9
17
+ Requires-Python: >=3.10
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
21
20
  Requires-Dist: SQLAlchemy>=2.0
@@ -123,6 +122,73 @@ The same command is available as a module:
123
122
  python -m sqlalchemyseed data.json --url sqlite:///app.db
124
123
  ```
125
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
+
126
192
  ## Documentation
127
193
 
128
194
  <https://sqlalchemyseed.readthedocs.io/>
@@ -98,6 +98,73 @@ The same command is available as a module:
98
98
  python -m sqlalchemyseed data.json --url sqlite:///app.db
99
99
  ```
100
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
+
101
168
  ## Documentation
102
169
 
103
170
  <https://sqlalchemyseed.readthedocs.io/>
@@ -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",
@@ -30,6 +29,9 @@ yaml = [
30
29
  [project.scripts]
31
30
  sqlalchemyseed = "sqlalchemyseed.cli:main"
32
31
 
32
+ [project.entry-points.pytest11]
33
+ sqlalchemyseed = "sqlalchemyseed.pytest_plugin"
34
+
33
35
  [project.urls]
34
36
  Homepage = "https://github.com/jedymatt/sqlalchemyseed"
35
37
  Documentation = "https://sqlalchemyseed.readthedocs.io/"
@@ -38,7 +40,9 @@ Tracker = "https://github.com/jedymatt/sqlalchemyseed/issues"
38
40
 
39
41
  [dependency-groups]
40
42
  dev = [
41
- "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",
42
46
  "coverage>=6.2",
43
47
  "PyYAML>=6.0",
44
48
  ]
@@ -11,7 +11,7 @@ from . import util
11
11
  from . import attribute
12
12
 
13
13
 
14
- __version__ = "2.2.0"
14
+ __version__ = "2.4.0"
15
15
 
16
16
  if __name__ == '__main__':
17
17
  pass
@@ -11,13 +11,6 @@ from sqlalchemy.orm import Session
11
11
  from . import loader
12
12
  from .seeder import HybridSeeder, Seeder
13
13
 
14
- _JSON_EXTENSIONS = {".json"}
15
- _YAML_EXTENSIONS = {".yaml", ".yml"}
16
- _CSV_EXTENSIONS = {".csv"}
17
- # Only self-describing formats are auto-discovered inside a directory. CSV
18
- # needs an explicit --model, so a CSV must be named as an individual file.
19
- _DISCOVERABLE_EXTENSIONS = _JSON_EXTENSIONS | _YAML_EXTENSIONS
20
-
21
14
 
22
15
  def build_parser() -> argparse.ArgumentParser:
23
16
  """Build the argument parser for the ``sqlalchemyseed`` command."""
@@ -55,6 +48,11 @@ def build_parser() -> argparse.ArgumentParser:
55
48
  action="store_true",
56
49
  help="seed within a transaction but roll back instead of committing",
57
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
+ )
58
56
  return parser
59
57
 
60
58
 
@@ -79,7 +77,7 @@ def _discover_directory(directory: Path) -> list:
79
77
  """Return the JSON/YAML files inside a directory, sorted by name."""
80
78
  discovered = sorted(
81
79
  child for child in directory.iterdir()
82
- if child.suffix.lower() in _DISCOVERABLE_EXTENSIONS
80
+ if child.suffix.lower() in loader.DISCOVERABLE_EXTENSIONS
83
81
  )
84
82
  if not discovered:
85
83
  raise FileNotFoundError(
@@ -88,25 +86,6 @@ def _discover_directory(directory: Path) -> list:
88
86
  return discovered
89
87
 
90
88
 
91
- def load_file(path: Path, model=None) -> dict:
92
- """Load entities from a single data file, dispatching on its extension."""
93
- suffix = path.suffix.lower()
94
- if suffix in _JSON_EXTENSIONS:
95
- return loader.load_entities_from_json(str(path))
96
- if suffix in _YAML_EXTENSIONS:
97
- return loader.load_entities_from_yaml(str(path))
98
- if suffix in _CSV_EXTENSIONS:
99
- return _load_csv(path, model)
100
- raise ValueError(f"unsupported file type: {path}")
101
-
102
-
103
- def _load_csv(path: Path, model) -> dict:
104
- """Load entities from a CSV file, which requires an explicit model."""
105
- if model is None:
106
- raise ValueError(f"CSV input requires --model to name the target class: {path}")
107
- return loader.load_entities_from_csv(str(path), model)
108
-
109
-
110
89
  def _make_seeder(name, session, ref_prefix):
111
90
  """Return the seeder implementation selected on the command line."""
112
91
  if name == "hybrid":
@@ -118,7 +97,7 @@ def _seed_all(seeder, files, model) -> int:
118
97
  """Seed every file through the seeder and return the entity count."""
119
98
  seeded = 0
120
99
  for path in files:
121
- seeder.seed(load_file(path, model))
100
+ seeder.seed(loader.load_path(path, model))
122
101
  seeded += len(seeder.instances)
123
102
  return seeded
124
103
 
@@ -141,17 +120,17 @@ def main(argv=None) -> int:
141
120
  except FileNotFoundError as error:
142
121
  parser.error(str(error))
143
122
 
144
- engine = sqlalchemy.create_engine(url)
145
- with Session(engine) as session:
146
- seeder = _make_seeder(args.seeder, session, args.ref_prefix)
147
- try:
123
+ try:
124
+ engine = sqlalchemy.create_engine(url)
125
+ with Session(engine) as session:
126
+ seeder = _make_seeder(args.seeder, session, args.ref_prefix)
148
127
  seeded = _seed_all(seeder, files, args.model)
149
- except Exception as error: # noqa: BLE001 - report any seeding failure as a clean exit
150
- session.rollback()
151
- print(f"error: {error}", file=sys.stderr)
152
- return 1
153
-
154
- return _finish(session, seeded, len(files), args.dry_run)
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
155
134
 
156
135
 
157
136
  def _finish(session, seeded, file_count, dry_run) -> int:
@@ -0,0 +1,87 @@
1
+ """
2
+ Text file loader module
3
+ """
4
+
5
+ import csv
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ try:
11
+ import yaml
12
+ except ModuleNotFoundError: # pragma: no cover
13
+ pass
14
+
15
+
16
+ def load_entities_from_json(json_filepath) -> dict:
17
+ """
18
+ Get entities from json
19
+ """
20
+ with open(json_filepath, 'r', encoding='utf-8') as file:
21
+ entities = json.loads(file.read())
22
+
23
+ return entities
24
+
25
+
26
+ def load_entities_from_yaml(yaml_filepath):
27
+ """
28
+ Get entities from yaml
29
+ """
30
+ if 'yaml' not in sys.modules:
31
+ raise ModuleNotFoundError(
32
+ 'PyYAML is not installed and is required to run this function. '
33
+ 'To use this function, py -m pip install "sqlalchemyseed[yaml]"'
34
+ )
35
+
36
+ with open(yaml_filepath, 'r', encoding='utf-8') as file:
37
+ entities = yaml.load(file.read(), Loader=yaml.SafeLoader)
38
+
39
+ return entities
40
+
41
+
42
+ def load_entities_from_csv(csv_filepath: str, model) -> dict:
43
+ """Load entities from csv file
44
+
45
+ :param csv_filepath: string csv file path
46
+ :param model: either str or class
47
+ :return: dict of entities
48
+ """
49
+ with open(csv_filepath, 'r', encoding='utf-8') as file:
50
+ source_data = list(
51
+ map(dict, csv.DictReader(file, skipinitialspace=True)))
52
+ if isinstance(model, str):
53
+ model_name = model
54
+ else:
55
+ model_name = '.'.join([model.__module__, model.__name__])
56
+
57
+ entities = {'model': model_name, 'data': source_data}
58
+
59
+ return entities
60
+
61
+
62
+ _JSON_EXTENSIONS = {".json"}
63
+ _YAML_EXTENSIONS = {".yaml", ".yml"}
64
+ _CSV_EXTENSIONS = {".csv"}
65
+ # Formats that are self-describing (carry their own model) and so can be
66
+ # auto-discovered inside a directory. CSV needs an explicit model.
67
+ DISCOVERABLE_EXTENSIONS = _JSON_EXTENSIONS | _YAML_EXTENSIONS
68
+
69
+
70
+ def load_path(path, model=None) -> dict:
71
+ """Load entities from a single data file, dispatching on its extension."""
72
+ path = Path(path)
73
+ suffix = path.suffix.lower()
74
+ if suffix in _JSON_EXTENSIONS:
75
+ return load_entities_from_json(str(path))
76
+ if suffix in _YAML_EXTENSIONS:
77
+ return load_entities_from_yaml(str(path))
78
+ if suffix in _CSV_EXTENSIONS:
79
+ return _load_csv(path, model)
80
+ raise ValueError(f"unsupported file type: {path}")
81
+
82
+
83
+ def _load_csv(path, model) -> dict:
84
+ """Load entities from a CSV file, which requires an explicit model."""
85
+ if model is None:
86
+ raise ValueError(f"CSV input requires a model to name the target class: {path}")
87
+ return load_entities_from_csv(str(path), model)
@@ -0,0 +1,66 @@
1
+ """pytest plugin: file-based SQLAlchemy fixtures with per-test rollback.
2
+
3
+ Registered via the ``pytest11`` entry point. Defines fixtures only — importing
4
+ this module has no side effects, and it is never imported by the package's
5
+ ``__init__`` so production imports never pull in pytest.
6
+ """
7
+
8
+ import pytest
9
+ from sqlalchemy.orm import Session
10
+
11
+ from .loader import load_path
12
+ from .seeder import HybridSeeder, Seeder
13
+
14
+ _MISSING_ENGINE_MESSAGE = (
15
+ "sqlalchemyseed's pytest plugin requires an 'engine' fixture. Define one in "
16
+ "your conftest.py that returns a SQLAlchemy Engine with your schema created, "
17
+ "e.g.:\n\n"
18
+ " @pytest.fixture(scope='session')\n"
19
+ " def engine():\n"
20
+ " engine = create_engine('sqlite:///test.db')\n"
21
+ " Base.metadata.create_all(engine)\n"
22
+ " return engine\n\n"
23
+ "For an in-memory SQLite database, and to have an explicit commit() rolled "
24
+ "back per test, see the docs for the full engine setup.\n"
25
+ )
26
+
27
+
28
+ @pytest.fixture
29
+ def engine():
30
+ """Placeholder the user overrides in their conftest with a real Engine."""
31
+ raise RuntimeError(_MISSING_ENGINE_MESSAGE)
32
+
33
+
34
+ @pytest.fixture
35
+ def sqlalchemyseed_session(engine):
36
+ """A Session in an open transaction that is rolled back after the test."""
37
+ connection = engine.connect()
38
+ transaction = connection.begin()
39
+ session = Session(bind=connection, join_transaction_mode="create_savepoint")
40
+ try:
41
+ yield session
42
+ finally:
43
+ # Nested so connection.close() always runs even if an earlier step
44
+ # raises — a leaked connection would otherwise exhaust the pool and
45
+ # surface as a failure in an unrelated later test.
46
+ try:
47
+ session.close()
48
+ finally:
49
+ try:
50
+ transaction.rollback()
51
+ finally:
52
+ connection.close()
53
+
54
+
55
+ @pytest.fixture
56
+ def seed(sqlalchemyseed_session):
57
+ """Return a callable that seeds a data file into the test session."""
58
+ def _seed(path, *, model=None, seeder="basic", ref_prefix="!"):
59
+ entities = load_path(path, model)
60
+ seeder_cls = HybridSeeder if seeder == "hybrid" else Seeder
61
+ instance = seeder_cls(sqlalchemyseed_session, ref_prefix=ref_prefix)
62
+ instance.seed(entities)
63
+ sqlalchemyseed_session.flush()
64
+ return instance
65
+
66
+ return _seed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemyseed
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: SQLAlchemy Seeder
5
5
  Author-email: Jedy Matt Tabasco <hello@jedymatt.dev>
6
6
  License-Expression: MIT
@@ -9,13 +9,12 @@ Project-URL: Documentation, https://sqlalchemyseed.readthedocs.io/
9
9
  Project-URL: Source, https://github.com/jedymatt/sqlalchemyseed
10
10
  Project-URL: Tracker, https://github.com/jedymatt/sqlalchemyseed/issues
11
11
  Keywords: sqlalchemy,orm,seed,seeder,json,yaml
12
- Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
16
  Classifier: Programming Language :: Python :: 3.14
18
- Requires-Python: >=3.9
17
+ Requires-Python: >=3.10
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
21
20
  Requires-Dist: SQLAlchemy>=2.0
@@ -123,6 +122,73 @@ The same command is available as a module:
123
122
  python -m sqlalchemyseed data.json --url sqlite:///app.db
124
123
  ```
125
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
+
126
192
  ## Documentation
127
193
 
128
194
  <https://sqlalchemyseed.readthedocs.io/>
@@ -10,6 +10,7 @@ src/sqlalchemyseed/dynamic_seeder.py
10
10
  src/sqlalchemyseed/errors.py
11
11
  src/sqlalchemyseed/json.py
12
12
  src/sqlalchemyseed/loader.py
13
+ src/sqlalchemyseed/pytest_plugin.py
13
14
  src/sqlalchemyseed/seeder.py
14
15
  src/sqlalchemyseed/util.py
15
16
  src/sqlalchemyseed/validator.py
@@ -24,6 +25,7 @@ src/sqlalchemyseed/_future/seeder.py
24
25
  tests/test_cli.py
25
26
  tests/test_json.py
26
27
  tests/test_loader.py
28
+ tests/test_pytest_plugin.py
27
29
  tests/test_seeder.py
28
30
  tests/test_temp_seeder.py
29
31
  tests/test_validator.py
@@ -1,2 +1,5 @@
1
1
  [console_scripts]
2
2
  sqlalchemyseed = sqlalchemyseed.cli:main
3
+
4
+ [pytest11]
5
+ sqlalchemyseed = sqlalchemyseed.pytest_plugin
@@ -68,7 +68,7 @@ def test_csv_without_model_fails(tmp_path, db_url, capsys):
68
68
  csv_file.write_text("name\nDave\n", encoding="utf-8")
69
69
 
70
70
  assert cli.main([str(csv_file), "--url", db_url]) == 1
71
- assert "requires --model" in capsys.readouterr().err
71
+ assert "requires a model" in capsys.readouterr().err
72
72
  assert count_persons(db_url) == 0
73
73
 
74
74
 
@@ -131,3 +131,19 @@ def test_unsupported_file_type(tmp_path, db_url):
131
131
  bad_file.write_text("nope", encoding="utf-8")
132
132
 
133
133
  assert cli.main([str(bad_file), "--url", db_url]) == 1
134
+
135
+
136
+ def test_error_message_includes_exception_type(tmp_path, db_url, capsys):
137
+ bad_file = tmp_path / "people.txt"
138
+ bad_file.write_text("nope", encoding="utf-8")
139
+
140
+ assert cli.main([str(bad_file), "--url", db_url]) == 1
141
+ assert "ValueError" in capsys.readouterr().err
142
+
143
+
144
+ def test_debug_flag_reraises_with_traceback(tmp_path, db_url):
145
+ bad_file = tmp_path / "people.txt"
146
+ bad_file.write_text("nope", encoding="utf-8")
147
+
148
+ with pytest.raises(ValueError):
149
+ cli.main([str(bad_file), "--url", db_url, "--debug"])
@@ -0,0 +1,77 @@
1
+ import unittest
2
+
3
+ import pytest
4
+
5
+ from sqlalchemyseed import loader
6
+ from sqlalchemyseed import load_entities_from_json
7
+ from sqlalchemyseed import load_entities_from_yaml
8
+ from sqlalchemyseed import load_entities_from_csv
9
+
10
+
11
+ def test_load_path_reads_json(tmp_path):
12
+ data_file = tmp_path / "d.json"
13
+ data_file.write_text('{"model": "m.M", "data": []}', encoding="utf-8")
14
+ assert loader.load_path(data_file) == {"model": "m.M", "data": []}
15
+
16
+
17
+ def test_load_path_reads_yaml(tmp_path):
18
+ data_file = tmp_path / "d.yml"
19
+ data_file.write_text("model: m.M\ndata: []\n", encoding="utf-8")
20
+ assert loader.load_path(data_file) == {"model": "m.M", "data": []}
21
+
22
+
23
+ def test_load_path_rejects_unknown_extension(tmp_path):
24
+ bad = tmp_path / "d.txt"
25
+ bad.write_text("nope", encoding="utf-8")
26
+ with pytest.raises(ValueError):
27
+ loader.load_path(bad)
28
+
29
+
30
+ def test_load_path_csv_requires_model(tmp_path):
31
+ csv_file = tmp_path / "d.csv"
32
+ csv_file.write_text("name\nAlice\n", encoding="utf-8")
33
+ with pytest.raises(ValueError):
34
+ loader.load_path(csv_file)
35
+
36
+
37
+ def test_discoverable_extensions_are_json_and_yaml():
38
+ assert loader.DISCOVERABLE_EXTENSIONS == {".json", ".yaml", ".yml"}
39
+
40
+
41
+ class TestLoader(unittest.TestCase):
42
+ def test_load_entities_from_json(self):
43
+ entities = load_entities_from_json('tests/res/data.json')
44
+ self.assertEqual(len(entities), 6)
45
+
46
+ def test_load_entities_from_json_file_not_found(self):
47
+ self.assertRaises(FileNotFoundError,
48
+ lambda: load_entities_from_json('tests/res/non-existent-file'))
49
+
50
+ def test_load_entities_from_yaml(self):
51
+ entities = load_entities_from_yaml('tests/res/data.yml')
52
+ self.assertEqual(len(entities), 2)
53
+
54
+ def test_load_entities_from_yaml_file_not_found(self):
55
+ self.assertRaises(FileNotFoundError,
56
+ lambda: load_entities_from_yaml('tests/res/non-existent-file'))
57
+
58
+ def test_load_entities_from_csv_input_class(self):
59
+ from tests.models import Company
60
+ entities = load_entities_from_csv(
61
+ 'tests/res/companies.csv', Company)
62
+ self.assertEqual(len(entities['data']), 3)
63
+
64
+ def test_load_entities_from_csv_input_model_string(self):
65
+ self.assertIsNotNone(load_entities_from_csv(
66
+ 'tests/res/companies.csv', "tests.models.Company"))
67
+
68
+ def test_loader_yaml_not_installed(self):
69
+ yaml_module = loader.sys.modules.pop('yaml')
70
+ try:
71
+ self.assertRaises(
72
+ ModuleNotFoundError,
73
+ lambda: load_entities_from_yaml('tests/res/data.yml')
74
+ )
75
+ finally:
76
+ loader.sys.modules['yaml'] = yaml_module
77
+
@@ -0,0 +1,331 @@
1
+ """Tests for the bundled pytest plugin, exercised via the pytester fixture."""
2
+
3
+ # A self-contained conftest for the inner pytest runs. StaticPool keeps a
4
+ # single in-memory SQLite connection alive so create_all and the session share
5
+ # one database.
6
+ CONFTEST = '''
7
+ import pytest
8
+ from sqlalchemy import create_engine, event
9
+ from sqlalchemy.pool import StaticPool
10
+
11
+ from models import Base
12
+
13
+
14
+ @pytest.fixture(scope="session")
15
+ def engine():
16
+ eng = create_engine(
17
+ "sqlite://",
18
+ connect_args={"check_same_thread": False},
19
+ poolclass=StaticPool,
20
+ )
21
+
22
+ # Hand transaction control to SQLAlchemy so an explicit commit() inside a
23
+ # test lands on a savepoint and is rolled back with the outer transaction.
24
+ # Left to itself the pysqlite driver commits straight to the database and
25
+ # the per-test rollback cannot undo it.
26
+ @event.listens_for(eng, "connect")
27
+ def _sqlite_no_driver_begin(dbapi_connection, connection_record):
28
+ dbapi_connection.isolation_level = None
29
+
30
+ @event.listens_for(eng, "begin")
31
+ def _sqlite_emit_begin(connection):
32
+ connection.exec_driver_sql("BEGIN")
33
+
34
+ Base.metadata.create_all(eng)
35
+ return eng
36
+ '''
37
+
38
+ MODELS = '''
39
+ from sqlalchemy import Column, ForeignKey, Integer, String
40
+ from sqlalchemy.orm import declarative_base, relationship
41
+
42
+ Base = declarative_base()
43
+
44
+
45
+ class Person(Base):
46
+ __tablename__ = "persons"
47
+ id = Column(Integer, primary_key=True)
48
+ name = Column(String(50))
49
+
50
+
51
+ class Company(Base):
52
+ __tablename__ = "companies"
53
+ id = Column(Integer, primary_key=True)
54
+ name = Column(String(50))
55
+ employees = relationship("Employee", back_populates="company")
56
+
57
+
58
+ class Employee(Base):
59
+ __tablename__ = "employees"
60
+ id = Column(Integer, primary_key=True)
61
+ name = Column(String(50))
62
+ company_id = Column(Integer, ForeignKey("companies.id"))
63
+ company = relationship("Company", back_populates="employees")
64
+ '''
65
+
66
+
67
+ def _scaffold(pytester):
68
+ """Write the shared conftest + models module into the inner project."""
69
+ pytester.makeconftest(CONFTEST)
70
+ pytester.makepyfile(models=MODELS)
71
+
72
+
73
+ def test_seed_json(pytester):
74
+ _scaffold(pytester)
75
+ pytester.makefile(
76
+ ".json",
77
+ people='{"model": "models.Person", "data": [{"name": "Alice"}, {"name": "Bob"}]}',
78
+ )
79
+ pytester.makepyfile(
80
+ test_seed_json='''
81
+ from models import Person
82
+
83
+ def test_it(seed, sqlalchemyseed_session):
84
+ seeder = seed("people.json")
85
+ assert sqlalchemyseed_session.query(Person).count() == 2
86
+ assert seeder.instances[0].name == "Alice"
87
+ '''
88
+ )
89
+ result = pytester.runpytest()
90
+ result.assert_outcomes(passed=1)
91
+
92
+
93
+ def test_seed_yaml(pytester):
94
+ _scaffold(pytester)
95
+ pytester.makefile(
96
+ ".yaml",
97
+ people="model: models.Person\ndata:\n - name: Carol\n",
98
+ )
99
+ pytester.makepyfile(
100
+ test_seed_yaml='''
101
+ from models import Person
102
+
103
+ def test_it(seed, sqlalchemyseed_session):
104
+ seed("people.yaml")
105
+ assert sqlalchemyseed_session.query(Person).count() == 1
106
+ '''
107
+ )
108
+ result = pytester.runpytest()
109
+ result.assert_outcomes(passed=1)
110
+
111
+
112
+ def test_seed_csv_with_model(pytester):
113
+ _scaffold(pytester)
114
+ pytester.makefile(".csv", people="name\nDave\nErin\n")
115
+ pytester.makepyfile(
116
+ test_seed_csv='''
117
+ from models import Person
118
+
119
+ def test_it(seed, sqlalchemyseed_session):
120
+ seed("people.csv", model="models.Person")
121
+ assert sqlalchemyseed_session.query(Person).count() == 2
122
+ '''
123
+ )
124
+ result = pytester.runpytest()
125
+ result.assert_outcomes(passed=1)
126
+
127
+
128
+ def test_hybrid_seeder(pytester):
129
+ _scaffold(pytester)
130
+ pytester.makefile(
131
+ ".json",
132
+ people='{"model": "models.Person", "data": [{"name": "Alice"}]}',
133
+ )
134
+ pytester.makepyfile(
135
+ test_hybrid='''
136
+ from models import Person
137
+
138
+ def test_it(seed, sqlalchemyseed_session):
139
+ seeder = seed("people.json", seeder="hybrid")
140
+ assert sqlalchemyseed_session.query(Person).count() == 1
141
+ assert seeder.instances[0].name == "Alice"
142
+ '''
143
+ )
144
+ result = pytester.runpytest()
145
+ result.assert_outcomes(passed=1)
146
+
147
+
148
+ def test_rollback_isolation(pytester):
149
+ _scaffold(pytester)
150
+ pytester.makefile(
151
+ ".json",
152
+ people='{"model": "models.Person", "data": [{"name": "Alice"}, {"name": "Bob"}]}',
153
+ )
154
+ pytester.makepyfile(
155
+ test_isolation='''
156
+ from models import Person
157
+
158
+ def test_first_seeds(seed, sqlalchemyseed_session):
159
+ seed("people.json")
160
+ assert sqlalchemyseed_session.query(Person).count() == 2
161
+
162
+ def test_second_sees_empty_db(sqlalchemyseed_session):
163
+ # The prior test's rows were rolled back with its transaction.
164
+ assert sqlalchemyseed_session.query(Person).count() == 0
165
+ '''
166
+ )
167
+ result = pytester.runpytest()
168
+ result.assert_outcomes(passed=2)
169
+
170
+
171
+ def test_committed_writes_still_roll_back(pytester):
172
+ # The point of join_transaction_mode="create_savepoint": even an explicit
173
+ # commit() lands on a savepoint inside the outer transaction and is unwound
174
+ # by the teardown rollback. This pins the documented contract that the
175
+ # flush-only seed path (test_rollback_isolation) does not exercise.
176
+ _scaffold(pytester)
177
+ pytester.makefile(
178
+ ".json",
179
+ people='{"model": "models.Person", "data": [{"name": "Alice"}]}',
180
+ )
181
+ pytester.makepyfile(
182
+ test_commit_isolation='''
183
+ from models import Person
184
+
185
+ def test_first_commits(seed, sqlalchemyseed_session):
186
+ seed("people.json")
187
+ sqlalchemyseed_session.commit()
188
+ assert sqlalchemyseed_session.query(Person).count() == 1
189
+
190
+ def test_second_sees_empty_db(sqlalchemyseed_session):
191
+ # The committed row was still rolled back with the outer transaction.
192
+ assert sqlalchemyseed_session.query(Person).count() == 0
193
+ '''
194
+ )
195
+ result = pytester.runpytest()
196
+ result.assert_outcomes(passed=2)
197
+
198
+
199
+ def test_reference_resolution_default_prefix(pytester):
200
+ # A "!"-prefixed key is a relationship reference; the seeder wires the
201
+ # nested Company onto the Employee. Guards the default ref_prefix.
202
+ _scaffold(pytester)
203
+ pytester.makefile(
204
+ ".json",
205
+ employee=(
206
+ '{"model": "models.Employee", "data": {"name": "Juan", '
207
+ '"!company": {"model": "models.Company", "data": {"name": "Acme"}}}}'
208
+ ),
209
+ )
210
+ pytester.makepyfile(
211
+ test_reference='''
212
+ from models import Company, Employee
213
+
214
+ def test_it(seed, sqlalchemyseed_session):
215
+ seed("employee.json")
216
+ employee = sqlalchemyseed_session.query(Employee).one()
217
+ assert employee.company.name == "Acme"
218
+ assert sqlalchemyseed_session.query(Company).count() == 1
219
+ '''
220
+ )
221
+ result = pytester.runpytest()
222
+ result.assert_outcomes(passed=1)
223
+
224
+
225
+ def test_custom_ref_prefix(pytester):
226
+ # A non-default prefix ("@") must be threaded through to the seeder so the
227
+ # "@company" key is treated as a reference rather than a column.
228
+ _scaffold(pytester)
229
+ pytester.makefile(
230
+ ".json",
231
+ employee=(
232
+ '{"model": "models.Employee", "data": {"name": "Mia", '
233
+ '"@company": {"model": "models.Company", "data": {"name": "Globex"}}}}'
234
+ ),
235
+ )
236
+ pytester.makepyfile(
237
+ test_prefix='''
238
+ from models import Employee
239
+
240
+ def test_it(seed, sqlalchemyseed_session):
241
+ seed("employee.json", ref_prefix="@")
242
+ employee = sqlalchemyseed_session.query(Employee).one()
243
+ assert employee.company.name == "Globex"
244
+ '''
245
+ )
246
+ result = pytester.runpytest()
247
+ result.assert_outcomes(passed=1)
248
+
249
+
250
+ def test_multiple_seed_calls_accumulate(pytester):
251
+ # Seeding several files in one test is a first-class use case; each call
252
+ # builds a fresh seeder on the shared session and flushes.
253
+ _scaffold(pytester)
254
+ pytester.makefile(
255
+ ".json",
256
+ alice='{"model": "models.Person", "data": [{"name": "Alice"}]}',
257
+ others='{"model": "models.Person", "data": [{"name": "Bob"}, {"name": "Carol"}]}',
258
+ )
259
+ pytester.makepyfile(
260
+ test_multi='''
261
+ from models import Person
262
+
263
+ def test_it(seed, sqlalchemyseed_session):
264
+ seed("alice.json")
265
+ seed("others.json")
266
+ assert sqlalchemyseed_session.query(Person).count() == 3
267
+ '''
268
+ )
269
+ result = pytester.runpytest()
270
+ result.assert_outcomes(passed=1)
271
+
272
+
273
+ def test_missing_engine_gives_actionable_error(pytester):
274
+ # Deliberately no conftest / engine fixture.
275
+ pytester.makepyfile(
276
+ test_no_engine='''
277
+ def test_it(seed):
278
+ pass
279
+ '''
280
+ )
281
+ result = pytester.runpytest()
282
+ result.assert_outcomes(errors=1)
283
+ result.stdout.fnmatch_lines(["*requires an 'engine' fixture*"])
284
+
285
+
286
+ def test_user_can_override_session(pytester):
287
+ _scaffold(pytester)
288
+ pytester.makefile(
289
+ ".json",
290
+ people='{"model": "models.Person", "data": [{"name": "Zoe"}]}',
291
+ )
292
+ # Override the plugin's session fixture; seed must use the override.
293
+ # A minimal session suffices here — this test proves override precedence,
294
+ # not the production fixture's transaction handling.
295
+ pytester.makepyfile(
296
+ test_override='''
297
+ import pytest
298
+ from sqlalchemy.orm import Session
299
+ from models import Person
300
+
301
+ @pytest.fixture
302
+ def sqlalchemyseed_session(engine):
303
+ session = Session(bind=engine)
304
+ session.info["overridden"] = True
305
+ yield session
306
+ session.close()
307
+
308
+ def test_it(seed, sqlalchemyseed_session):
309
+ seed("people.json")
310
+ assert sqlalchemyseed_session.info.get("overridden") is True
311
+ assert sqlalchemyseed_session.query(Person).count() == 1
312
+ '''
313
+ )
314
+ result = pytester.runpytest()
315
+ result.assert_outcomes(passed=1)
316
+
317
+
318
+ def test_unsupported_file_type_fails_loudly(pytester):
319
+ _scaffold(pytester)
320
+ pytester.makefile(".txt", data="nope")
321
+ pytester.makepyfile(
322
+ test_unsupported='''
323
+ import pytest
324
+
325
+ def test_it(seed):
326
+ with pytest.raises(ValueError):
327
+ seed("data.txt")
328
+ '''
329
+ )
330
+ result = pytester.runpytest()
331
+ result.assert_outcomes(passed=1)
@@ -1,64 +0,0 @@
1
- """
2
- Text file loader module
3
- """
4
-
5
- import csv
6
- import json
7
- import sys
8
-
9
- try:
10
- import yaml
11
- except ModuleNotFoundError: # pragma: no cover
12
- pass
13
-
14
-
15
- def load_entities_from_json(json_filepath) -> dict:
16
- """
17
- Get entities from json
18
- """
19
- try:
20
- with open(json_filepath, 'r', encoding='utf-8') as file:
21
- entities = json.loads(file.read())
22
- except FileNotFoundError as error:
23
- raise FileNotFoundError from error
24
-
25
- return entities
26
-
27
-
28
- def load_entities_from_yaml(yaml_filepath):
29
- """
30
- Get entities from yaml
31
- """
32
- if 'yaml' not in sys.modules:
33
- raise ModuleNotFoundError(
34
- 'PyYAML is not installed and is required to run this function. '
35
- 'To use this function, py -m pip install "sqlalchemyseed[yaml]"'
36
- )
37
-
38
- try:
39
- with open(yaml_filepath, 'r', encoding='utf-8') as file:
40
- entities = yaml.load(file.read(), Loader=yaml.SafeLoader)
41
- except FileNotFoundError as error:
42
- raise FileNotFoundError from error
43
-
44
- return entities
45
-
46
-
47
- def load_entities_from_csv(csv_filepath: str, model) -> dict:
48
- """Load entities from csv file
49
-
50
- :param csv_filepath: string csv file path
51
- :param model: either str or class
52
- :return: dict of entities
53
- """
54
- with open(csv_filepath, 'r', encoding='utf-8') as file:
55
- source_data = list(
56
- map(dict, csv.DictReader(file, skipinitialspace=True)))
57
- if isinstance(model, str):
58
- model_name = model
59
- else:
60
- model_name = '.'.join([model.__module__, model.__name__])
61
-
62
- entities = {'model': model_name, 'data': source_data}
63
-
64
- return entities
@@ -1,41 +0,0 @@
1
- import unittest
2
- from sqlalchemyseed import loader
3
-
4
- from src.sqlalchemyseed import load_entities_from_json
5
- from src.sqlalchemyseed import load_entities_from_yaml
6
- from src.sqlalchemyseed import load_entities_from_csv
7
-
8
- class TestLoader(unittest.TestCase):
9
- def test_load_entities_from_json(self):
10
- entities = load_entities_from_json('tests/res/data.json')
11
- self.assertEqual(len(entities), 6)
12
-
13
- def test_load_entities_from_json_file_not_found(self):
14
- self.assertRaises(FileNotFoundError,
15
- lambda: load_entities_from_json('tests/res/non-existent-file'))
16
-
17
- def test_load_entities_from_yaml(self):
18
- entities = load_entities_from_yaml('tests/res/data.yml')
19
- self.assertEqual(len(entities), 2)
20
-
21
- def test_load_entities_from_yaml_file_not_found(self):
22
- self.assertRaises(FileNotFoundError,
23
- lambda: load_entities_from_yaml('tests/res/non-existent-file'))
24
-
25
- def test_load_entities_from_csv_input_class(self):
26
- from tests.models import Company
27
- entities = load_entities_from_csv(
28
- 'tests/res/companies.csv', Company)
29
- self.assertEqual(len(entities['data']), 3)
30
-
31
- def test_load_entities_from_csv_input_model_string(self):
32
- self.assertIsNotNone(load_entities_from_csv(
33
- 'tests/res/companies.csv', "tests.models.Company"))
34
-
35
- def test_loader_yaml_not_installed(self):
36
- loader.sys.modules.pop('yaml')
37
- self.assertRaises(
38
- ModuleNotFoundError,
39
- lambda: load_entities_from_yaml('tests/res/data.yml')
40
- )
41
-
File without changes
File without changes