rowsmyth 0.1.0__py3-none-any.whl

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.
rowsmyth/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from importlib.metadata import version
2
+
3
+ from ._base import TableSpecMixin, declarative_base
4
+ from ._builder import FactoryBuilder
5
+ from ._dataset import Dataset
6
+ from ._variant import variant
7
+
8
+ __version__ = version("rowsmyth")
9
+ __all__ = [
10
+ "Dataset",
11
+ "FactoryBuilder",
12
+ "TableSpecMixin",
13
+ "__version__",
14
+ "declarative_base",
15
+ "variant",
16
+ ]
rowsmyth/_base.py ADDED
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from sqlalchemy.orm import DeclarativeBase
6
+ from sqlalchemy.orm import registry as _registry
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+ from sqlalchemy import MetaData
12
+
13
+ from ._builder import FactoryBuilder
14
+ from ._dataset import Dataset
15
+
16
+
17
+ # SQLAlchemy's declared_attr only works for instance-level access, so we use a
18
+ # custom descriptor to expose table metadata as class-level read-only properties.
19
+ class _classproperty: # noqa: N801
20
+ def __init__(self, func: Callable[..., Any]) -> None:
21
+ self.func = func
22
+
23
+ def __get__(self, obj: Any, cls: type | None = None) -> Any:
24
+ return self.func(cls if cls is not None else type(obj))
25
+
26
+
27
+ class TableSpecMixin:
28
+ __variants__: dict[str, Callable[..., Any]]
29
+
30
+ def __init_subclass__(cls, **kwargs: Any) -> None:
31
+ super().__init_subclass__(**kwargs)
32
+ cls.__variants__ = {
33
+ name: val
34
+ for name, val in vars(cls).items()
35
+ if callable(val) and getattr(val, "_is_variant", False)
36
+ }
37
+
38
+ @_classproperty
39
+ def __comment__(cls) -> str | None: # noqa: N805
40
+ return cls.__table__.comment # ty: ignore[unresolved-attribute]
41
+
42
+ @_classproperty
43
+ def __table_info__(cls) -> dict[str, Any]: # noqa: N805
44
+ return cls.__table__.info # ty: ignore[unresolved-attribute]
45
+
46
+ @_classproperty
47
+ def __column_info__(cls) -> dict[str, dict[str, Any]]: # noqa: N805
48
+ return {
49
+ prop.key: col.info
50
+ for prop in cls.__mapper__.column_attrs # ty: ignore[unresolved-attribute]
51
+ for col in prop.columns
52
+ }
53
+
54
+ @_classproperty
55
+ def __expectations__(cls) -> dict[str, str]: # noqa: N805
56
+ from sqlalchemy import CheckConstraint
57
+
58
+ return {
59
+ c.name: str(c.sqltext)
60
+ for c in cls.__table__.constraints # ty: ignore[unresolved-attribute]
61
+ if isinstance(c, CheckConstraint) and c.name is not None
62
+ }
63
+
64
+ @_classproperty
65
+ def __spark_schema__(cls) -> Any: # noqa: N805
66
+ from ._spark import to_spark_schema
67
+
68
+ return to_spark_schema(cls) # ty: ignore[invalid-argument-type]
69
+
70
+ def __repr__(self) -> str:
71
+ attrs = ", ".join(
72
+ f"{c.key}={getattr(self, c.key)!r}"
73
+ for c in self.__mapper__.columns # ty: ignore[unresolved-attribute]
74
+ )
75
+ return f"{self.__class__.__name__}({attrs})"
76
+
77
+ @classmethod
78
+ def generators(cls) -> dict[Any, Any]:
79
+ """Override to supply factory-boy declarations for column generation.
80
+
81
+ Returns:
82
+ A dict mapping column attributes (or string names) to factory-boy
83
+ declarations (e.g. ``factory.Faker(...)``). Keys can be the column
84
+ attribute itself (``cls.name``) or a plain string (``"name"``).
85
+
86
+ Example:
87
+ >>> TableSpecMixin.generators()
88
+ {}
89
+ """
90
+ return {}
91
+
92
+ @classmethod
93
+ def factory(cls, count: int = 1, max_count: int | None = None) -> FactoryBuilder:
94
+ """Return a FactoryBuilder for this model.
95
+
96
+ Args:
97
+ count: Exact number of instances to generate, or minimum when
98
+ ``max_count`` is also provided.
99
+ max_count: Upper bound for a random count in
100
+ ``[count, max_count]`` inclusive. ``None`` means exact.
101
+
102
+ Returns:
103
+ A ``FactoryBuilder`` ready to be configured with ``.has()``,
104
+ ``.mix()``, ``.where()`` and ``.create()``.
105
+
106
+ Example:
107
+ >>> # User.factory(10) # exactly 10
108
+ >>> # User.factory(1, 5) # random 1-5 per parent
109
+ >>> # User.factory(5).create() # generate and persist
110
+ """ # doctest: +SKIP
111
+ from ._builder import FactoryBuilder
112
+
113
+ return FactoryBuilder(cls, count, max_count)
114
+
115
+ @classmethod
116
+ def dataset(cls, *builders: FactoryBuilder) -> Dataset:
117
+ """Return a Dataset generating one row per registered model.
118
+
119
+ Args:
120
+ *builders: Optional ``FactoryBuilder`` overrides for specific
121
+ models. Models without an override get one instance using
122
+ their default generators.
123
+
124
+ Returns:
125
+ A ``Dataset`` whose ``.create()`` returns a dict keyed by table
126
+ name, values are lists of persisted model instances.
127
+
128
+ Raises:
129
+ TypeError: If any argument is not a ``FactoryBuilder``.
130
+ ValueError: If a builder's model is not registered to this Base.
131
+
132
+ Example:
133
+ >>> # result = Base.dataset(User.factory(10)).create()
134
+ >>> # result["users"] # -> list of 10 User instances
135
+ """ # doctest: +SKIP
136
+ from ._builder import FactoryBuilder
137
+ from ._dataset import Dataset
138
+
139
+ base_models = {mapper.class_ for mapper in cls.registry.mappers} # ty: ignore[unresolved-attribute]
140
+ for b in builders:
141
+ if not isinstance(b, FactoryBuilder):
142
+ msg = f"Expected a FactoryBuilder, got {type(b).__name__}"
143
+ raise TypeError(msg)
144
+ if b.model not in base_models:
145
+ msg = f"{b.model.__name__} is not registered to this Base."
146
+ raise ValueError(msg)
147
+ overrides = {b.model: b for b in builders}
148
+ return Dataset(cls, overrides)
149
+
150
+
151
+ def declarative_base(
152
+ metadata: MetaData | None = None,
153
+ type_annotation_map: dict[Any, Any] | None = None,
154
+ registry: _registry | None = None,
155
+ ) -> type[TableSpecMixin]:
156
+ """Create a new SQLAlchemy declarative base with rowsmyth capabilities.
157
+
158
+ Args:
159
+ metadata: Optional ``MetaData`` instance shared by all models that
160
+ subclass the returned base. A new ``MetaData`` is created if
161
+ not provided.
162
+ type_annotation_map: Optional mapping of Python types to SQLAlchemy
163
+ ``TypeEngine`` classes or instances, used by ``Mapped[]``
164
+ annotations.
165
+ registry: Optional pre-existing ``registry`` instance. When provided,
166
+ models using this base share the mapper registry with other bases
167
+ that reference the same registry.
168
+
169
+ Returns:
170
+ A new declarative base class with ``TableSpecMixin`` mixed in.
171
+
172
+ Example:
173
+ >>> from rowsmyth import declarative_base
174
+ >>> Base = declarative_base()
175
+ >>> issubclass(Base, TableSpecMixin)
176
+ True
177
+ """
178
+ attrs: dict[str, Any] = {}
179
+ if metadata is not None:
180
+ attrs["metadata"] = metadata
181
+ if type_annotation_map is not None:
182
+ attrs["type_annotation_map"] = type_annotation_map
183
+ if registry is not None:
184
+ attrs["registry"] = registry
185
+ return type("Base", (TableSpecMixin, DeclarativeBase), attrs)
rowsmyth/_builder.py ADDED
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from typing import Any
5
+
6
+
7
+ class FactoryBuilder:
8
+ """Fluent builder for generating and persisting model instances.
9
+
10
+ Obtain via ``Model.factory(count)``. Chain configuration methods,
11
+ then call ``.create()`` to generate and persist to an in-memory database.
12
+
13
+ Example:
14
+ >>> # users = User.factory(10).has(Order.factory(1, 3)).create()
15
+ """ # doctest: +SKIP
16
+
17
+ def __init__(
18
+ self, model: type, count: int = 1, max_count: int | None = None
19
+ ) -> None:
20
+ self.model = model
21
+ self.min_count = count
22
+ self.max_count = max_count
23
+ self._children: list[tuple[FactoryBuilder, str | None]] = []
24
+ self._mix: dict[str, float] = {}
25
+ self._overrides: dict[str, Any] = {}
26
+ self._seed: int | None = None
27
+
28
+ def has(self, *builders: FactoryBuilder, via: str | None = None) -> FactoryBuilder:
29
+ """Attach child builders whose rows are created for each parent instance.
30
+
31
+ Args:
32
+ *builders: ``FactoryBuilder`` instances for child models.
33
+ via: Relationship attribute name on the child model to use when
34
+ wiring the FK. Only required when the child has multiple
35
+ relationships pointing to the same parent model.
36
+
37
+ Returns:
38
+ ``self`` for chaining.
39
+
40
+ Example:
41
+ >>> # User.factory(5).has(Order.factory(1, 3))
42
+ """ # doctest: +SKIP
43
+ for builder in builders:
44
+ self._children.append((builder, via))
45
+ return self
46
+
47
+ def mix(self, **proportions: float) -> FactoryBuilder:
48
+ """Set variant proportions for this builder.
49
+
50
+ Args:
51
+ **proportions: Variant name to proportion in ``(0, 1]``. Total
52
+ must not exceed 1.0. The remainder (``1 - sum``) is the
53
+ proportion of un-varied (default) instances.
54
+
55
+ Returns:
56
+ ``self`` for chaining.
57
+
58
+ Raises:
59
+ ValueError: If proportions sum to more than 1.0, or a named
60
+ variant is not defined on the model.
61
+
62
+ Example:
63
+ >>> # User.factory(100).mix(admin=0.05, premium=0.20)
64
+ """ # doctest: +SKIP
65
+ for name, proportion in proportions.items():
66
+ if proportion <= 0.0:
67
+ msg = f"Proportion for '{name}' must be > 0, got {proportion}"
68
+ raise ValueError(msg)
69
+ total = sum(proportions.values())
70
+ if total > 1.0:
71
+ msg = f"Proportions sum to {total:.2f}, must be ≤ 1.0"
72
+ raise ValueError(msg)
73
+ for name in proportions:
74
+ if name not in self.model.__variants__: # ty: ignore[unresolved-attribute]
75
+ msg = (
76
+ f"Variant '{name}' not defined on {self.model.__name__}. "
77
+ f"Defined: {list(self.model.__variants__)}" # ty: ignore[unresolved-attribute]
78
+ )
79
+ raise ValueError(msg)
80
+ self._mix = proportions
81
+ return self
82
+
83
+ def _resolve_count(self) -> int:
84
+ if self.max_count is None:
85
+ return self.min_count
86
+ return random.randint(self.min_count, self.max_count)
87
+
88
+ def _pick_variant(self) -> str | None:
89
+ if not self._mix:
90
+ return None
91
+ r = random.random()
92
+ cumulative = 0.0
93
+ for name, proportion in self._mix.items():
94
+ cumulative += proportion
95
+ if r < cumulative:
96
+ return name
97
+ # Remainder proportion (1 - sum) maps to no variant. When proportions
98
+ # sum to exactly 1.0, floating-point imprecision may leave a tiny gap
99
+ # here for a negligible fraction of calls - that's acceptable.
100
+ return None
101
+
102
+ def random_seed(self, value: int) -> FactoryBuilder:
103
+ """Fix the random seed for reproducible output.
104
+
105
+ Args:
106
+ value: Integer seed passed to both ``random`` and ``Faker``.
107
+
108
+ Returns:
109
+ ``self`` for chaining.
110
+
111
+ Example:
112
+ >>> # User.factory(10).random_seed(42).create()
113
+ """ # doctest: +SKIP
114
+ self._seed = value
115
+ return self
116
+
117
+ def where(self, overrides: dict[Any, Any]) -> FactoryBuilder:
118
+ """Apply fixed column overrides to every generated instance.
119
+
120
+ Args:
121
+ overrides: Dict mapping column attributes (or string names) to
122
+ fixed values. These take precedence over generators and variants.
123
+
124
+ Returns:
125
+ ``self`` for chaining.
126
+
127
+ Example:
128
+ >>> # User.factory(5).where({User.tier: "premium"})
129
+ """ # doctest: +SKIP
130
+ self._overrides.update(overrides)
131
+ return self
132
+
133
+ def create(self) -> list[Any]:
134
+ """Generate and persist all instances to an in-memory SQLite database.
135
+
136
+ Returns:
137
+ List of root model instances. Child instances (added via ``.has()``)
138
+ are persisted and accessible via SQLAlchemy relationships but are
139
+ not included in the returned list.
140
+
141
+ Example:
142
+ >>> # users = User.factory(10).has(Order.factory(2)).create()
143
+ >>> # len(users) # -> 10
144
+ """ # doctest: +SKIP
145
+ from ._execute import execute_builder
146
+
147
+ return execute_builder(self, self._overrides, self._seed)
rowsmyth/_dataset.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import graphlib
4
+ import random
5
+ from typing import Any
6
+
7
+ from sqlalchemy.orm import MANYTOONE
8
+
9
+ from ._execute import (
10
+ _build_fk_dependency_graph,
11
+ _resolve_variant,
12
+ _seed_random,
13
+ build_session,
14
+ make_factory,
15
+ )
16
+
17
+
18
+ class Dataset:
19
+ """Generates one instance per registered model, respecting FK order.
20
+
21
+ Obtain via ``Base.dataset(*builders)``. Use when you need a minimal
22
+ coherent dataset across all tables rather than a deep hierarchy rooted
23
+ at one model.
24
+
25
+ Example:
26
+ >>> # result = Base.dataset(User.factory(5), Order.factory(10)).create()
27
+ >>> # result["users"] # -> 5 User instances
28
+ >>> # result["orders"] # -> 10 Order instances
29
+ """ # doctest: +SKIP
30
+
31
+ def __init__(self, base: type, overrides: dict[type, Any] | None = None) -> None:
32
+ self._base = base
33
+ self._overrides: dict[type, Any] = overrides or {}
34
+ self._seed: int | None = None
35
+
36
+ def random_seed(self, value: int) -> Dataset:
37
+ """Fix the random seed for reproducible output.
38
+
39
+ Args:
40
+ value: Integer seed passed to both ``random`` and ``Faker``.
41
+
42
+ Returns:
43
+ ``self`` for chaining.
44
+ """
45
+ self._seed = value
46
+ return self
47
+
48
+ def create(self) -> dict[str, list[Any]]:
49
+ """Generate and persist one instance per registered model.
50
+
51
+ Instances are created in FK dependency order (parents before children).
52
+ FK columns are wired automatically by sampling randomly from the parent pool.
53
+
54
+ Returns:
55
+ Dict keyed by table name mapping to lists of persisted instances.
56
+
57
+ Raises:
58
+ ValueError: If a parent model has 0 instances but a child model
59
+ has an FK pointing to it.
60
+ """
61
+ from ._builder import FactoryBuilder
62
+
63
+ if self._seed is not None:
64
+ _seed_random(self._seed)
65
+
66
+ builders = []
67
+ for mapper in self._base.registry.mappers: # ty: ignore[unresolved-attribute]
68
+ model = mapper.class_
69
+ builders.append(self._overrides.get(model, FactoryBuilder(model, 1)))
70
+
71
+ ordered = self._topo_sort(builders)
72
+ models = {b.model for b in ordered}
73
+ session = build_session(models)
74
+ factories = {m: make_factory(m, session) for m in models}
75
+
76
+ pool: dict[type, list[Any]] = {}
77
+ result: dict[str, list[Any]] = {}
78
+
79
+ for builder in ordered:
80
+ instances: list[Any] = []
81
+ for _ in range(builder._resolve_count()):
82
+ fk_overrides: dict[str, Any] = {}
83
+ for rel in builder.model.__mapper__.relationships:
84
+ if rel.direction is not MANYTOONE:
85
+ continue
86
+ related = rel.mapper.class_
87
+ if related not in pool:
88
+ continue
89
+ if not pool[related]:
90
+ model_name = builder.model.__name__
91
+ related_name = related.__name__
92
+ msg = (
93
+ f"Cannot wire {model_name}.{rel.key}: "
94
+ f"{related_name} has 0 instances in the dataset. "
95
+ f"Use {related_name}.factory(0) only for models "
96
+ f"with no FK dependents."
97
+ )
98
+ raise ValueError(msg)
99
+ fk_overrides[rel.key] = random.choice(pool[related])
100
+
101
+ inst_overrides = _resolve_variant(builder)
102
+ inst_overrides.update(fk_overrides)
103
+
104
+ instance = factories[builder.model](**inst_overrides)
105
+ instances.append(instance)
106
+
107
+ pool[builder.model] = instances
108
+ result[builder.model.__tablename__] = instances
109
+
110
+ return result
111
+
112
+ def _topo_sort(self, builders: list[Any]) -> list[Any]:
113
+ model_to_builder = {b.model: b for b in builders}
114
+ graph = _build_fk_dependency_graph(builders)
115
+ ts = graphlib.TopologicalSorter(graph)
116
+ ordered_models = list(ts.static_order())
117
+ return [model_to_builder[m] for m in ordered_models if m in model_to_builder]
rowsmyth/_execute.py ADDED
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import graphlib
5
+ import random
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from factory.alchemy import SQLAlchemyModelFactory
9
+ from sqlalchemy import Table, create_engine
10
+ from sqlalchemy.orm import MANYTOONE, Session
11
+
12
+ if TYPE_CHECKING:
13
+ from ._builder import FactoryBuilder
14
+
15
+
16
+ def _seed_random(seed: int) -> None:
17
+ from faker import Faker
18
+
19
+ random.seed(seed)
20
+ Faker.seed(seed)
21
+
22
+
23
+ def check_cycles(builder: FactoryBuilder) -> None:
24
+ graph: dict[type, set[type]] = {}
25
+
26
+ def _collect(b: FactoryBuilder) -> None:
27
+ if b.model in graph:
28
+ return
29
+ graph[b.model] = set()
30
+ for child_builder, _ in b._children:
31
+ graph[b.model].add(child_builder.model)
32
+ _collect(child_builder)
33
+
34
+ _collect(builder)
35
+ ts = graphlib.TopologicalSorter(graph)
36
+ ts.prepare()
37
+
38
+
39
+ def _build_fk_dependency_graph(builders: list[FactoryBuilder]) -> dict[type, set[type]]:
40
+ model_set = {b.model for b in builders}
41
+ return {
42
+ b.model: {
43
+ rel.mapper.class_
44
+ for rel in b.model.__mapper__.relationships # ty: ignore[unresolved-attribute]
45
+ if rel.direction is MANYTOONE
46
+ and rel.mapper.class_ in model_set
47
+ and rel.mapper.class_ is not b.model
48
+ }
49
+ for b in builders
50
+ }
51
+
52
+
53
+ @functools.cache
54
+ def _get_default_builders(model: type) -> dict[str, FactoryBuilder]:
55
+ from ._builder import FactoryBuilder
56
+
57
+ return {
58
+ getattr(key, "key", key): val
59
+ for key, val in model.generators().items() # ty: ignore[unresolved-attribute]
60
+ if isinstance(val, FactoryBuilder)
61
+ }
62
+
63
+
64
+ def collect_models(builder: FactoryBuilder) -> set[type]:
65
+ models: set[type] = set()
66
+
67
+ def _collect(b: FactoryBuilder) -> None:
68
+ models.add(b.model)
69
+ for child_builder, _ in b._children:
70
+ _collect(child_builder)
71
+ for default_builder in _get_default_builders(b.model).values():
72
+ _collect(default_builder)
73
+
74
+ _collect(builder)
75
+ return models
76
+
77
+
78
+ def collect_all_tables(models: set[type]) -> set[Table]:
79
+ seen: set[Table] = {m.__table__ for m in models} # ty: ignore[unresolved-attribute]
80
+ queue: list[Table] = list(seen)
81
+ while queue:
82
+ table = queue.pop()
83
+ for fk in table.foreign_key_constraints:
84
+ for col in fk.elements:
85
+ ref_table = col.column.table
86
+ if ref_table not in seen:
87
+ seen.add(ref_table)
88
+ queue.append(ref_table)
89
+ return seen
90
+
91
+
92
+ def _resolve_variant(builder: FactoryBuilder) -> dict[str, Any]:
93
+ variant_name = builder._pick_variant()
94
+ if not variant_name:
95
+ return {}
96
+ raw = builder.model.__variants__[variant_name](builder.model) # ty: ignore[unresolved-attribute]
97
+ return _resolve_attr_names(raw)
98
+
99
+
100
+ def _resolve_attr_names(d: dict[Any, Any]) -> dict[str, Any]:
101
+ result: dict[str, Any] = {}
102
+ for key, val in d.items():
103
+ attr_name = getattr(key, "key", key)
104
+ result[attr_name] = val
105
+ return result
106
+
107
+
108
+ def build_session(models: set[type]) -> Session:
109
+ tables = collect_all_tables(models)
110
+ metadata = next(iter(models)).__table__.metadata # ty: ignore[unresolved-attribute]
111
+ engine = create_engine("sqlite:///:memory:")
112
+ metadata.create_all(engine, tables=list(tables))
113
+ return Session(engine)
114
+
115
+
116
+ def make_factory(model: type, session: Session) -> type[SQLAlchemyModelFactory]:
117
+ from ._builder import FactoryBuilder
118
+
119
+ raw_generators = {
120
+ k: v
121
+ for k, v in model.generators().items() # ty: ignore[unresolved-attribute]
122
+ if not isinstance(v, FactoryBuilder)
123
+ }
124
+ generators = _resolve_attr_names(raw_generators)
125
+ meta = type(
126
+ "Meta",
127
+ (),
128
+ {
129
+ "model": model,
130
+ "sqlalchemy_session": session,
131
+ "sqlalchemy_session_persistence": "commit",
132
+ },
133
+ )
134
+ attrs: dict[str, Any] = {"Meta": meta, **generators}
135
+ return type(f"{model.__name__}Factory", (SQLAlchemyModelFactory,), attrs)
136
+
137
+
138
+ @functools.cache
139
+ def find_relationship(
140
+ child_model: type, parent_model: type, via: str | None = None
141
+ ) -> str:
142
+ if via is not None:
143
+ return via
144
+ matches = [
145
+ rel.key
146
+ for rel in child_model.__mapper__.relationships # ty: ignore[unresolved-attribute]
147
+ if rel.mapper.class_ is parent_model
148
+ ]
149
+ if not matches:
150
+ msg = (
151
+ f"No relationship from {child_model.__name__} to {parent_model.__name__}. "
152
+ f"Define one via SQLAlchemy relationship() or pass via= to .has()"
153
+ )
154
+ raise ValueError(msg)
155
+ if len(matches) > 1:
156
+ msg = (
157
+ f"Ambiguous - found {matches} on {child_model.__name__} pointing to "
158
+ f"{parent_model.__name__}. Pass via='rel_name' to .has()"
159
+ )
160
+ raise ValueError(msg)
161
+ return matches[0]
162
+
163
+
164
+ def _create_instance(
165
+ builder: FactoryBuilder,
166
+ extra_overrides: dict[str, Any],
167
+ factories: dict[type, type[SQLAlchemyModelFactory]],
168
+ ) -> Any:
169
+ inst_overrides = _resolve_variant(builder)
170
+ inst_overrides.update(extra_overrides)
171
+
172
+ default_builders = _get_default_builders(builder.model)
173
+ for rel in builder.model.__mapper__.relationships: # ty: ignore[unresolved-attribute]
174
+ if rel.direction is not MANYTOONE:
175
+ continue
176
+ if rel.key not in inst_overrides and rel.key in default_builders:
177
+ inst_overrides[rel.key] = _create_instance(
178
+ default_builders[rel.key], {}, factories
179
+ )
180
+
181
+ instance = factories[builder.model](**inst_overrides)
182
+
183
+ for child_builder, via in builder._children:
184
+ rel_key = find_relationship(child_builder.model, builder.model, via)
185
+ for _ in range(child_builder._resolve_count()):
186
+ _create_instance(child_builder, {rel_key: instance}, factories)
187
+
188
+ return instance
189
+
190
+
191
+ def execute_builder(
192
+ builder: FactoryBuilder,
193
+ overrides: dict[Any, Any],
194
+ seed: int | None = None,
195
+ ) -> list[Any]:
196
+ if seed is not None:
197
+ _seed_random(seed)
198
+
199
+ check_cycles(builder)
200
+ models = collect_models(builder)
201
+ session = build_session(models)
202
+ factories = {m: make_factory(m, session) for m in models}
203
+ root_overrides = _resolve_attr_names(overrides)
204
+
205
+ return [
206
+ _create_instance(builder, root_overrides, factories)
207
+ for _ in range(builder._resolve_count())
208
+ ]