Skip to content

Commit 9780440

Browse files
authored
Merge pull request #5 from jedymatt/v0.4.x
V0.4.x
2 parents 840dca2 + dec4e87 commit 9780440

File tree

7 files changed

+297
-68
lines changed

7 files changed

+297
-68
lines changed

.github/workflows/python-package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ main ]
8+
branches: [ v0.4.x ]
99
pull_request:
10-
branches: [ main ]
10+
branches: [ v0.4.x ]
1111

1212
jobs:
1313
build:

README.md

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![PyPI - License](https://img.shields.io/pypi/l/sqlalchemyseed)](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
66
[![Python package](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml/badge.svg)](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)
77

8-
Sqlalchemy seeder. Supports nested relationships.
8+
Sqlalchemy seeder that supports nested relationships.
99

1010
## Installation
1111

@@ -20,10 +20,9 @@ pip install sqlalchemyseed
2020
## Getting Started
2121

2222
```python
23-
2423
# main.py
2524
from sqlalchemyseed import load_entities_from_json, Seeder
26-
from tests.db import session
25+
from db import session
2726

2827
# load entities
2928
entities = load_entities_from_json('tests/test_data.json')
@@ -37,29 +36,28 @@ seeder.seed(entities)
3736

3837
# Committing
3938
session.commit() # or seeder.session.commit()
40-
41-
4239
```
4340

4441
## Seeder vs. HybridSeeder
4542

46-
- Seeder supports model and data key fields.
47-
- Seeder does not support model and filter key fields.
48-
- Seeder when seeding, can specify to not add all entities to session by passing this argument `add_to_session=False` in the `Seeder.seed` method.
49-
- HybridSeeder supports 'model' and 'data', and 'model' and 'filter' key fields.
50-
- HybridSeeder enables to query existing objects from the session and assigns it with the relationship.
51-
- HybridSeeder when seeding, automatically adds all entities to session.
43+
| Features & Options | Seeder | HybridSeeder |
44+
| :--------------------------------------------------------------------- | :----------------- | :----------------- |
45+
| Support `model` and `data` keys | :heavy_check_mark: | :heavy_check_mark: |
46+
| Support `model` and `filter` keys | :x: | :heavy_check_mark: |
47+
| Optional argument `add_to_session=False` in the `seed` method | :heavy_check_mark: | :x: |
48+
| Assign existing objects from session or db to a relationship attribute | :x: | :heavy_check_mark: |
5249

5350
## When to use HybridSeeder and 'filter' key field?
5451

55-
```python
52+
Assuming that `Child(age=5)` exists in the database or session,
53+
then we should use *filter* instead of *data*,
54+
the values of *filter* will query from the database or session,
55+
and assign it to the `Parent.child`
5656

57+
```python
5758
from sqlalchemyseed import HybridSeeder
58-
from tests.db import session
59+
from db import session
5960

60-
# Assuming that Child(age=5) exists in the database or session,
61-
# then we should use 'filter' instead of 'obj'
62-
# the the values of 'filter' will query from the database or session, and assign it to the Parent.child
6361
data = {
6462
"model": "models.Parent",
6563
"data": {
@@ -77,9 +75,57 @@ data = {
7775
seeder = HybridSeeder(session)
7876
seeder.seed(data)
7977

78+
session.commit() # or seeder.sesssion.commit()
79+
```
80+
81+
## Relationships
82+
83+
In adding a relationship attribute, add prefix **!** to the key in order to identify it.
84+
85+
### Referencing relationship object or a foreign key
86+
87+
If your class don't have a relationship attribute but instead a foreign key attribute you can use it the same as how you did it on a relationship attribute
88+
89+
```python
90+
from sqlalchemyseed import HybridSeeder
91+
from db import session
92+
93+
instance = [
94+
{
95+
'model': 'tests.models.Company',
96+
'data': {'name': 'MyCompany'}
97+
},
98+
{
99+
'model': 'tests.models.Employee',
100+
'data':[
101+
{
102+
'name': 'John Smith',
103+
# foreign key attribute
104+
'!company_id': {
105+
'model': 'tests.models.Company',
106+
'filter': {
107+
'name': 'MyCompany'
108+
}
109+
}
110+
},
111+
{
112+
'name': 'Juan Dela Cruz',
113+
# relationship attribute
114+
'!company': {
115+
'model': 'tests.models.Company',
116+
'filter': {
117+
'name': 'MyCompany'
118+
}
119+
}
120+
]
121+
}
122+
]
123+
124+
seeder = HybridSeeder(session)
125+
seeder.seed(instance)
80126
```
81127

82-
## No Relationship
128+
### No Relationship
83129

84130
```json5
85131
// test_data.json
@@ -108,10 +154,6 @@ seeder.seed(data)
108154
]
109155
```
110156

111-
## Relationships
112-
113-
In adding a relationship attribute, add prefix '!' to the key in order to identify it.
114-
115157
### One to One
116158

117159
```json5
@@ -178,7 +220,7 @@ In adding a relationship attribute, add prefix '!' to the key in order to identi
178220
]
179221
```
180222

181-
## Example of Nested Relationships
223+
### Example of Nested Relationships
182224

183225
```json
184226
{
@@ -203,5 +245,4 @@ In adding a relationship attribute, add prefix '!' to the key in order to identi
203245
]
204246
}
205247
}
206-
207248
```

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# TODO
22

3+
- [x] HybridSeeder filter from foreign key id
34
- [x] HybridSeeder
45
- [x] Seeder
56
- [x] Validator

sqlalchemyseed/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .seeder import HybridSeeder
22
from .seeder import Seeder
3+
from .seeder import ClassRegistry
34

45

5-
__version__ = '0.3.2'
6+
__version__ = '0.4.0'

sqlalchemyseed/seeder.py

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from inspect import isclass
44

55
import sqlalchemy.orm
6-
from sqlalchemy import inspect
6+
from sqlalchemy import Table, column, inspect, select, table, text
77
from sqlalchemy.exc import NoInspectionAvailable
8-
from sqlalchemy.orm import RelationshipProperty
8+
from sqlalchemy.orm import ColumnProperty, RelationshipProperty
9+
from sqlalchemy.orm.relationships import foreign
910

1011
from . import validator
1112

@@ -93,14 +94,14 @@ def seed(self, instance, add_to_session=True):
9394
if add_to_session is True:
9495
self._session.add_all(self.instances)
9596

96-
def _pre_seed(self, instance, parent=None, parent_attr=None):
97+
def _pre_seed(self, instance, parent=None, parent_attr_name=None):
9798
if isinstance(instance, list):
9899
for i in instance:
99-
self._seed(i, parent, parent_attr)
100+
self._seed(i, parent, parent_attr_name)
100101
else:
101-
self._seed(instance, parent, parent_attr)
102+
self._seed(instance, parent, parent_attr_name)
102103

103-
def _seed(self, instance: dict, parent=None, parent_attr=None):
104+
def _seed(self, instance: dict, parent=None, parent_attr_name=None):
104105
keys = None
105106
for r_keys in self._required_keys:
106107
if all(key in instance.keys() for key in r_keys):
@@ -118,49 +119,59 @@ def _seed(self, instance: dict, parent=None, parent_attr=None):
118119

119120
if isinstance(instance[keys[1]], list):
120121
for value in instance[keys[1]]:
121-
obj = self.instantiate_obj(class_path, value, key_is_data)
122-
# print(obj, parent, parent_attr)
123-
if parent is not None and parent_attr is not None:
124-
attr_ = getattr(parent.__class__, parent_attr)
125-
if attr_.property.uselist is True:
126-
if getattr(parent, parent_attr) is None:
127-
setattr(parent, parent_attr, [])
128-
129-
getattr(parent, parent_attr).append(obj)
122+
obj = self.instantiate_obj(
123+
class_path, value, key_is_data, parent, parent_attr_name)
124+
# print(obj, parent, parent_attr_name)
125+
if parent is not None and parent_attr_name is not None:
126+
attr_ = getattr(parent.__class__, parent_attr_name)
127+
if isinstance(attr_.property, RelationshipProperty):
128+
if attr_.property.uselist is True:
129+
if getattr(parent, parent_attr_name) is None:
130+
setattr(parent, parent_attr_name, [])
131+
132+
getattr(parent, parent_attr_name).append(obj)
133+
else:
134+
setattr(parent, parent_attr_name, obj)
130135
else:
131-
setattr(parent, parent_attr, obj)
136+
setattr(parent, parent_attr_name, obj)
132137
else:
133-
self._instances.append(obj)
138+
if inspect(obj.__class__) and key_is_data is True:
139+
self._instances.append(obj)
134140
# check for relationships
135141
for k, v in value.items():
136142
if str(k).startswith('!'):
137-
self._pre_seed(v, obj, k[1:])
143+
self._pre_seed(v, obj, k[1:]) # removed prefix
138144

139145
elif isinstance(instance[keys[1]], dict):
140146
obj = self.instantiate_obj(
141-
class_path, instance[keys[1]], key_is_data)
142-
# print(parent, parent_attr)
143-
if parent is not None and parent_attr is not None:
144-
attr_ = getattr(parent.__class__, parent_attr)
145-
if attr_.property.uselist is True:
146-
if getattr(parent, parent_attr) is None:
147-
setattr(parent, parent_attr, [])
148-
149-
getattr(parent, parent_attr).append(obj)
147+
class_path, instance[keys[1]], key_is_data, parent, parent_attr_name)
148+
# print(parent, parent_attr_name)
149+
if parent is not None and parent_attr_name is not None:
150+
attr_ = getattr(parent.__class__, parent_attr_name)
151+
if isinstance(attr_.property, RelationshipProperty):
152+
if attr_.property.uselist is True:
153+
if getattr(parent, parent_attr_name) is None:
154+
setattr(parent, parent_attr_name, [])
155+
156+
getattr(parent, parent_attr_name).append(obj)
157+
else:
158+
setattr(parent, parent_attr_name, obj)
150159
else:
151-
setattr(parent, parent_attr, obj)
160+
setattr(parent, parent_attr_name, obj)
152161
else:
153-
self._instances.append(obj)
162+
if inspect(obj.__class__) and key_is_data is True:
163+
self._instances.append(obj)
164+
154165
# check for relationships
155166
for k, v in instance[keys[1]].items():
156167
# print(k, v)
157168
if str(k).startswith('!'):
158169
# print(k)
159-
self._pre_seed(v, obj, k[1:])
170+
self._pre_seed(v, obj, k[1:]) # removed prefix '!'
160171

161172
return instance
162173

163-
def instantiate_obj(self, class_path, kwargs, key_is_data):
174+
def instantiate_obj(self, class_path, kwargs, key_is_data, parent=None, parent_attr_name=None):
164175
class_ = self._class_registry[class_path]
165176

166177
filtered_kwargs = {k: v for k, v in kwargs.items() if
@@ -180,18 +191,55 @@ def __init__(self, session: sqlalchemy.orm.Session):
180191
('model', 'filter')
181192
]
182193

183-
def seed(self, instance, **kwargs):
194+
def seed(self, instance):
184195
super().seed(instance, False)
185196

186-
def instantiate_obj(self, class_path, kwargs, key_is_data=True):
197+
def instantiate_obj(self, class_path, kwargs, key_is_data, parent, parent_attr_name):
198+
"""Instantiates or queries object, or queries ForeignKey
199+
200+
Args:
201+
class_path (str): Class path
202+
kwargs ([dict]): Class kwargs
203+
key_is_data (bool): key is 'data'
204+
parent (object): parent object
205+
parent_attr_name (str): parent attribute name
206+
207+
Returns:
208+
Any: instantiated object or queried oject, or foreign key id
209+
"""
210+
187211
class_ = self._class_registry[class_path]
188212

189213
filtered_kwargs = {k: v for k, v in kwargs.items() if
190214
not k.startswith('!') and not isinstance(getattr(class_, k), RelationshipProperty)}
191215

192216
if key_is_data is True:
217+
if parent is not None and parent_attr_name is not None:
218+
class_attr = getattr(parent.__class__, parent_attr_name)
219+
if isinstance(class_attr.property, ColumnProperty):
220+
raise TypeError('invalid class attribute type')
221+
193222
obj = class_(**filtered_kwargs)
194223
self._session.add(obj)
224+
# self._session.flush()
195225
return obj
196226
else:
227+
if parent is not None and parent_attr_name is not None:
228+
class_attr = getattr(parent.__class__, parent_attr_name)
229+
if isinstance(class_attr.property, ColumnProperty):
230+
foreign_key = str(
231+
list(getattr(parent.__class__, parent_attr_name).foreign_keys)[0].column)
232+
foreign_key_id=self._query_instance_id(
233+
class_, filtered_kwargs, foreign_key)
234+
return foreign_key_id
235+
197236
return self._session.query(class_).filter_by(**filtered_kwargs).one()
237+
238+
def _query_instance_id(self, class_, filtered_kwargs, foreign_key):
239+
# .id should be the foreign key
240+
arr=foreign_key.rsplit('.')
241+
column_name=arr[len(arr)-1]
242+
243+
result=self.session.query(
244+
getattr(class_, column_name)).filter_by(**filtered_kwargs).one()
245+
return getattr(result, column_name)

tests/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,32 @@ class Employee(Base):
3030

3131
def __repr__(self) -> str:
3232
return f"<Employee(name='{self.name}', company='{self.company}')>"
33+
34+
35+
class Parent(Base):
36+
__tablename__ = 'parents'
37+
38+
id = Column(Integer, primary_key=True)
39+
name = Column(String(255))
40+
children = relationship('Child')
41+
42+
def __repr__(self) -> str:
43+
return f"<Parent(name='{self.name}', children='{[child.name if child.name is not None else child for child in self.children]}')>"
44+
45+
46+
class Child(Base):
47+
__tablename__ = 'children'
48+
49+
id = Column(Integer, primary_key=True)
50+
name = Column(String(255))
51+
parent_id = Column(Integer, ForeignKey('parents.id'))
52+
53+
children = relationship('GrandChild')
54+
55+
56+
class GrandChild(Base):
57+
__tablename__ = 'grand_children'
58+
59+
id = Column(Integer, primary_key=True)
60+
name = Column(String(255))
61+
parent_id = Column(Integer, ForeignKey('children.id'))

0 commit comments

Comments
 (0)