Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Python package

on:
push:
branches: [ main ]
branches: [ v0.4.x ]
pull_request:
branches: [ main ]
branches: [ v0.4.x ]

jobs:
build:
Expand Down
87 changes: 64 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![PyPI - License](https://img.shields.io/pypi/l/sqlalchemyseed)](https://github.com/jedymatt/sqlalchemyseed/blob/main/LICENSE)
[![Python package](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml/badge.svg)](https://github.com/jedymatt/sqlalchemyseed/actions/workflows/python-package.yml)

Sqlalchemy seeder. Supports nested relationships.
Sqlalchemy seeder that supports nested relationships.

## Installation

Expand All @@ -20,10 +20,9 @@ pip install sqlalchemyseed
## Getting Started

```python

# main.py
from sqlalchemyseed import load_entities_from_json, Seeder
from tests.db import session
from db import session

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

# Committing
session.commit() # or seeder.session.commit()


```

## Seeder vs. HybridSeeder

- Seeder supports model and data key fields.
- Seeder does not support model and filter key fields.
- 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.
- HybridSeeder supports 'model' and 'data', and 'model' and 'filter' key fields.
- HybridSeeder enables to query existing objects from the session and assigns it with the relationship.
- HybridSeeder when seeding, automatically adds all entities to session.
| Features & Options | Seeder | HybridSeeder |
| :--------------------------------------------------------------------- | :----------------- | :----------------- |
| Support `model` and `data` keys | :heavy_check_mark: | :heavy_check_mark: |
| Support `model` and `filter` keys | :x: | :heavy_check_mark: |
| Optional argument `add_to_session=False` in the `seed` method | :heavy_check_mark: | :x: |
| Assign existing objects from session or db to a relationship attribute | :x: | :heavy_check_mark: |

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

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

```python
from sqlalchemyseed import HybridSeeder
from tests.db import session
from db import session

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

session.commit() # or seeder.sesssion.commit()
```

## Relationships

In adding a relationship attribute, add prefix **!** to the key in order to identify it.

### Referencing relationship object or a foreign key

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

```python
from sqlalchemyseed import HybridSeeder
from db import session

instance = [
{
'model': 'tests.models.Company',
'data': {'name': 'MyCompany'}
},
{
'model': 'tests.models.Employee',
'data':[
{
'name': 'John Smith',
# foreign key attribute
'!company_id': {
'model': 'tests.models.Company',
'filter': {
'name': 'MyCompany'
}
}
},
{
'name': 'Juan Dela Cruz',
# relationship attribute
'!company': {
'model': 'tests.models.Company',
'filter': {
'name': 'MyCompany'
}
}
]
}
]

seeder = HybridSeeder(session)
seeder.seed(instance)
```

## No Relationship
### No Relationship

```json5
// test_data.json
Expand Down Expand Up @@ -108,10 +154,6 @@ seeder.seed(data)
]
```

## Relationships

In adding a relationship attribute, add prefix '!' to the key in order to identify it.

### One to One

```json5
Expand Down Expand Up @@ -178,7 +220,7 @@ In adding a relationship attribute, add prefix '!' to the key in order to identi
]
```

## Example of Nested Relationships
### Example of Nested Relationships

```json
{
Expand All @@ -203,5 +245,4 @@ In adding a relationship attribute, add prefix '!' to the key in order to identi
]
}
}

```
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# TODO

- [x] HybridSeeder filter from foreign key id
- [x] HybridSeeder
- [x] Seeder
- [x] Validator
Expand Down
3 changes: 2 additions & 1 deletion sqlalchemyseed/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .seeder import HybridSeeder
from .seeder import Seeder
from .seeder import ClassRegistry


__version__ = '0.3.2'
__version__ = '0.4.0'
114 changes: 81 additions & 33 deletions sqlalchemyseed/seeder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from inspect import isclass

import sqlalchemy.orm
from sqlalchemy import inspect
from sqlalchemy import Table, column, inspect, select, table, text
from sqlalchemy.exc import NoInspectionAvailable
from sqlalchemy.orm import RelationshipProperty
from sqlalchemy.orm import ColumnProperty, RelationshipProperty
from sqlalchemy.orm.relationships import foreign

from . import validator

Expand Down Expand Up @@ -93,14 +94,14 @@ def seed(self, instance, add_to_session=True):
if add_to_session is True:
self._session.add_all(self.instances)

def _pre_seed(self, instance, parent=None, parent_attr=None):
def _pre_seed(self, instance, parent=None, parent_attr_name=None):
if isinstance(instance, list):
for i in instance:
self._seed(i, parent, parent_attr)
self._seed(i, parent, parent_attr_name)
else:
self._seed(instance, parent, parent_attr)
self._seed(instance, parent, parent_attr_name)

def _seed(self, instance: dict, parent=None, parent_attr=None):
def _seed(self, instance: dict, parent=None, parent_attr_name=None):
keys = None
for r_keys in self._required_keys:
if all(key in instance.keys() for key in r_keys):
Expand All @@ -118,49 +119,59 @@ def _seed(self, instance: dict, parent=None, parent_attr=None):

if isinstance(instance[keys[1]], list):
for value in instance[keys[1]]:
obj = self.instantiate_obj(class_path, value, key_is_data)
# print(obj, parent, parent_attr)
if parent is not None and parent_attr is not None:
attr_ = getattr(parent.__class__, parent_attr)
if attr_.property.uselist is True:
if getattr(parent, parent_attr) is None:
setattr(parent, parent_attr, [])

getattr(parent, parent_attr).append(obj)
obj = self.instantiate_obj(
class_path, value, key_is_data, parent, parent_attr_name)
# print(obj, parent, parent_attr_name)
if parent is not None and parent_attr_name is not None:
attr_ = getattr(parent.__class__, parent_attr_name)
if isinstance(attr_.property, RelationshipProperty):
if attr_.property.uselist is True:
if getattr(parent, parent_attr_name) is None:
setattr(parent, parent_attr_name, [])

getattr(parent, parent_attr_name).append(obj)
else:
setattr(parent, parent_attr_name, obj)
else:
setattr(parent, parent_attr, obj)
setattr(parent, parent_attr_name, obj)
else:
self._instances.append(obj)
if inspect(obj.__class__) and key_is_data is True:
self._instances.append(obj)
# check for relationships
for k, v in value.items():
if str(k).startswith('!'):
self._pre_seed(v, obj, k[1:])
self._pre_seed(v, obj, k[1:]) # removed prefix

elif isinstance(instance[keys[1]], dict):
obj = self.instantiate_obj(
class_path, instance[keys[1]], key_is_data)
# print(parent, parent_attr)
if parent is not None and parent_attr is not None:
attr_ = getattr(parent.__class__, parent_attr)
if attr_.property.uselist is True:
if getattr(parent, parent_attr) is None:
setattr(parent, parent_attr, [])

getattr(parent, parent_attr).append(obj)
class_path, instance[keys[1]], key_is_data, parent, parent_attr_name)
# print(parent, parent_attr_name)
if parent is not None and parent_attr_name is not None:
attr_ = getattr(parent.__class__, parent_attr_name)
if isinstance(attr_.property, RelationshipProperty):
if attr_.property.uselist is True:
if getattr(parent, parent_attr_name) is None:
setattr(parent, parent_attr_name, [])

getattr(parent, parent_attr_name).append(obj)
else:
setattr(parent, parent_attr_name, obj)
else:
setattr(parent, parent_attr, obj)
setattr(parent, parent_attr_name, obj)
else:
self._instances.append(obj)
if inspect(obj.__class__) and key_is_data is True:
self._instances.append(obj)

# check for relationships
for k, v in instance[keys[1]].items():
# print(k, v)
if str(k).startswith('!'):
# print(k)
self._pre_seed(v, obj, k[1:])
self._pre_seed(v, obj, k[1:]) # removed prefix '!'

return instance

def instantiate_obj(self, class_path, kwargs, key_is_data):
def instantiate_obj(self, class_path, kwargs, key_is_data, parent=None, parent_attr_name=None):
class_ = self._class_registry[class_path]

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

def seed(self, instance, **kwargs):
def seed(self, instance):
super().seed(instance, False)

def instantiate_obj(self, class_path, kwargs, key_is_data=True):
def instantiate_obj(self, class_path, kwargs, key_is_data, parent, parent_attr_name):
"""Instantiates or queries object, or queries ForeignKey

Args:
class_path (str): Class path
kwargs ([dict]): Class kwargs
key_is_data (bool): key is 'data'
parent (object): parent object
parent_attr_name (str): parent attribute name

Returns:
Any: instantiated object or queried oject, or foreign key id
"""

class_ = self._class_registry[class_path]

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

if key_is_data is True:
if parent is not None and parent_attr_name is not None:
class_attr = getattr(parent.__class__, parent_attr_name)
if isinstance(class_attr.property, ColumnProperty):
raise TypeError('invalid class attribute type')

obj = class_(**filtered_kwargs)
self._session.add(obj)
# self._session.flush()
return obj
else:
if parent is not None and parent_attr_name is not None:
class_attr = getattr(parent.__class__, parent_attr_name)
if isinstance(class_attr.property, ColumnProperty):
foreign_key = str(
list(getattr(parent.__class__, parent_attr_name).foreign_keys)[0].column)
foreign_key_id=self._query_instance_id(
class_, filtered_kwargs, foreign_key)
return foreign_key_id

return self._session.query(class_).filter_by(**filtered_kwargs).one()

def _query_instance_id(self, class_, filtered_kwargs, foreign_key):
# .id should be the foreign key
arr=foreign_key.rsplit('.')
column_name=arr[len(arr)-1]

result=self.session.query(
getattr(class_, column_name)).filter_by(**filtered_kwargs).one()
return getattr(result, column_name)
29 changes: 29 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,32 @@ class Employee(Base):

def __repr__(self) -> str:
return f"<Employee(name='{self.name}', company='{self.company}')>"


class Parent(Base):
__tablename__ = 'parents'

id = Column(Integer, primary_key=True)
name = Column(String(255))
children = relationship('Child')

def __repr__(self) -> str:
return f"<Parent(name='{self.name}', children='{[child.name if child.name is not None else child for child in self.children]}')>"


class Child(Base):
__tablename__ = 'children'

id = Column(Integer, primary_key=True)
name = Column(String(255))
parent_id = Column(Integer, ForeignKey('parents.id'))

children = relationship('GrandChild')


class GrandChild(Base):
__tablename__ = 'grand_children'

id = Column(Integer, primary_key=True)
name = Column(String(255))
parent_id = Column(Integer, ForeignKey('children.id'))
Loading