From 38babd641e0db032581cee9e4cbdeb75f0e5efea Mon Sep 17 00:00:00 2001 From: Abhishek Ram Date: Mon, 11 Mar 2019 21:57:29 +0530 Subject: [PATCH] Changes to sync with Protean 0.0.9 (#7) * add the lookups and function to build the filters * add test cases for all the lookups * add test cases for q object * add support for Relation and Associations * change the test command to include flake8 * bump the version of protean * changes to travis and isort corrections * add correct distro for 3.7 --- .travis.yml | 4 +- CHANGELOG.rst | 6 + requirements.txt | 2 +- requirements/test.txt | 1 + setup.py | 4 +- src/protean_sqlalchemy/__init__.py | 2 +- src/protean_sqlalchemy/repository.py | 221 +++++++++++++++++++++++---- src/protean_sqlalchemy/sa.py | 23 ++- tests/support/dog.py | 43 ++++++ tests/support/human.py | 55 +++++++ tests/test_filters_lookups.py | 193 +++++++++++++++++++++++ tests/test_relations.py | 75 +++++++++ tests/test_repo_ext.py | 38 +---- tests/test_repository.py | 44 ++---- 14 files changed, 603 insertions(+), 108 deletions(-) create mode 100644 tests/support/dog.py create mode 100644 tests/support/human.py create mode 100644 tests/test_filters_lookups.py create mode 100644 tests/test_relations.py diff --git a/.travis.yml b/.travis.yml index 014a4d5..8b92dad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: python +dist: xenial python: - '3.6' + - '3.7' install: - python setup.py install - pip install -I -r requirements/test.txt script: - - pytest --cov=protean_sqlalchemy --cov-config .coveragerc tests + - pytest --cov=protean_sqlalchemy --cov-config .coveragerc --flake8 after_success: - pip install codecov - codecov diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94c77ec..a11d68f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,3 +18,9 @@ Changelog ------------------ * Match with version 0.0.7 of protean + + +0.0.9 (2019-03-11) +------------------ + +* Match with version 0.0.9 of protean diff --git a/requirements.txt b/requirements.txt index 0ff70bf..c13dbf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -protean==0.0.7 +protean==0.0.9 click==7.0 sqlalchemy==1.2.14 -r requirements/dev.txt diff --git a/requirements/test.txt b/requirements/test.txt index 7a97627..23e3ebe 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -5,3 +5,4 @@ pytest==3.6.3 pytest-cov==2.5.1 pluggy==0.6.0 pytest-mock==1.10.0 +pytest-flake8==1.0.4 diff --git a/setup.py b/setup.py index 3091650..7055c45 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def read(*names, **kwargs): setup( name='protean-sqlalchemy', - version='0.0.7', + version='0.0.9', license='BSD 3-Clause License', description='Protean Sqlalachemy Extension', long_description='%s\n%s' % ( @@ -64,7 +64,7 @@ def read(*names, **kwargs): ], install_requires=[ 'click==7.0', - 'protean==0.0.7', + 'protean==0.0.9', 'sqlalchemy==1.2.14' # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/src/protean_sqlalchemy/__init__.py b/src/protean_sqlalchemy/__init__.py index 2792152..9d1ffab 100644 --- a/src/protean_sqlalchemy/__init__.py +++ b/src/protean_sqlalchemy/__init__.py @@ -1 +1 @@ -__version__ = '0.0.7' +__version__ = '0.0.9' diff --git a/src/protean_sqlalchemy/repository.py b/src/protean_sqlalchemy/repository.py index 22765f2..49e6b0d 100644 --- a/src/protean_sqlalchemy/repository.py +++ b/src/protean_sqlalchemy/repository.py @@ -1,11 +1,16 @@ """This module holds the definition of Database connectivity""" +from protean.core import field from protean.core.exceptions import ConfigurationError from protean.core.repository import BaseAdapter from protean.core.repository import BaseConnectionHandler from protean.core.repository import BaseModel +from protean.core.repository import Lookup from protean.core.repository import Pagination +from protean.utils.query import Q +from sqlalchemy import and_ from sqlalchemy import create_engine +from sqlalchemy import or_ from sqlalchemy import orm from sqlalchemy.engine.url import make_url from sqlalchemy.exc import DatabaseError @@ -52,13 +57,21 @@ def __tablename__(cls): @classmethod def from_entity(cls, entity): """ Convert the entity to a model object """ - return cls(**entity.to_dict()) + item_dict = {} + for field_obj in cls.opts_.entity_cls.declared_fields.values(): + if isinstance(field_obj, field.Reference): + item_dict[field_obj.relation.field_name] = \ + field_obj.relation.value + else: + item_dict[field_obj.field_name] = getattr( + entity, field_obj.field_name) + return cls(**item_dict) @classmethod def to_entity(cls, model_obj): """ Convert the model object to an entity """ item_dict = {} - for field_name in cls.opts_.entity_cls.meta_.declared_fields: + for field_name in cls.opts_.entity_cls.declared_fields: item_dict[field_name] = getattr(model_obj, field_name, None) return cls.opts_.entity_cls(item_dict) @@ -66,33 +79,38 @@ def to_entity(cls, model_obj): class Adapter(BaseAdapter): """Adapter implementation for the Databases compliant with SQLAlchemy""" - def _filter_objects(self, page: int = 1, per_page: int = 10, # noqa: C901 - order_by: list = (), excludes_: dict = None, - **filters) -> Pagination: - """ Filter objects from the sqlalchemy database """ - qs = self.conn.query(self.model_cls) + def _build_filters(self, criteria: Q): + """ Recursively Build the filters from the criteria object""" + # Decide the function based on the connector type + func = and_ if criteria.connector == criteria.AND else or_ + params = [] + for child in criteria.children: + if isinstance(child, Q): + # Call the function again with the child + params.append(self._build_filters(child)) + else: + # Find the lookup class and the key + stripped_key, lookup_class = self._extract_lookup(child[0]) - # check for sqlalchemy filters - filter_ = filters.pop('filter_', None) - if filter_ is not None: - qs = qs.filter(filter_) + # Instantiate the lookup class and get the expression + lookup = lookup_class(stripped_key, child[1], self.model_cls) + if criteria.negated: + params.append(~lookup.as_expression()) + else: + params.append(lookup.as_expression()) - # apply the rest of the filters and excludes - for fk, fv in filters.items(): - col = getattr(self.model_cls, fk) - if type(fv) in (list, tuple): - qs = qs.filter(col.in_(fv)) - else: - qs = qs.filter(col == fv) + return func(*params) - for ek, ev in excludes_.items(): - col = getattr(self.model_cls, ek) - if type(ev) in (list, tuple): - qs = qs.filter(~col.in_(ev)) - else: - qs = qs.filter(col != ev) + def _filter_objects(self, criteria: Q, page: int = 1, per_page: int = 10, + order_by: list = ()) -> Pagination: + """ Filter objects from the sqlalchemy database """ + qs = self.conn.query(self.model_cls) - # apply the ordering + # Build the filters from the criteria + if criteria.children: + qs = qs.filter(self._build_filters(criteria)) + + # Apply the order by clause if present order_cols = [] for order_col in order_by: col = getattr(self.model_cls, order_col.lstrip('-')) @@ -109,12 +127,13 @@ def _filter_objects(self, page: int = 1, per_page: int = 10, # noqa: C901 # Return the results try: - total = qs.count() + items = qs.all() + total = qs.count() if per_page > 0 else len(items) result = Pagination( page=page, per_page=per_page, total=total, - items=qs.all()) + items=items) except DatabaseError: self.conn.rollback() raise @@ -127,7 +146,7 @@ def _create_object(self, model_obj): try: # If the model has Auto fields then flush to get them - if self.entity_cls.meta_.has_auto_field: + if self.entity_cls.auto_fields: self.conn.flush() self.conn.commit() except DatabaseError: @@ -140,13 +159,17 @@ def _update_object(self, model_obj): """ Update a record in the sqlalchemy database""" primary_key, data = {}, {} for field_name, field_obj in \ - self.entity_cls.meta_.declared_fields.items(): + self.entity_cls.declared_fields.items(): if field_obj.identifier: primary_key = { field_name: getattr(model_obj, field_name) } else: - data[field_name] = getattr(model_obj, field_name, None) + if isinstance(field_obj, field.Reference): + data[field_obj.relation.field_name] = \ + field_obj.relation.value + else: + data[field_name] = getattr(model_obj, field_name, None) # Run the update query and commit the results try: @@ -170,3 +193,141 @@ def _delete_objects(self, **filters): self.conn.rollback() raise return del_count + + +operators = { + 'exact': '__eq__', + 'iexact': 'ilike', + 'contains': 'contains', + 'icontains': 'ilike', + 'startswith': 'startswith', + 'endswith': 'endswith', + 'gt': '__gt__', + 'gte': '__ge__', + 'lt': '__lt__', + 'lte': '__le__', + 'in': 'in_', + 'overlap': 'overlap', + 'any': 'any', +} + + +class DefaultLookup(Lookup): + """Base class with default implementation of expression construction""" + + def __init__(self, source, target, model_cls): + """Source is LHS and Target is RHS of a comparsion""" + self.model_cls = model_cls + super().__init__(source, target) + + def process_source(self): + """Return source with transformations, if any""" + source_col = getattr(self.model_cls, self.source) + return source_col + + def process_target(self): + """Return target with transformations, if any""" + return self.target + + def as_expression(self): + lookup_func = getattr(self.process_source(), + operators[self.lookup_name]) + return lookup_func(self.process_target()) + + +@Adapter.register_lookup +class Exact(DefaultLookup): + """Exact Match Query""" + lookup_name = 'exact' + + +@Adapter.register_lookup +class IExact(DefaultLookup): + """Exact Case-Insensitive Match Query""" + lookup_name = 'iexact' + + +@Adapter.register_lookup +class Contains(DefaultLookup): + """Exact Contains Query""" + lookup_name = 'contains' + + +@Adapter.register_lookup +class IContains(DefaultLookup): + """Exact Case-Insensitive Contains Query""" + lookup_name = 'icontains' + + def process_target(self): + """Return target in lowercase""" + assert isinstance(self.target, str) + return f"%{super().process_target()}%" + + +@Adapter.register_lookup +class Startswith(DefaultLookup): + """Exact Contains Query""" + lookup_name = 'startswith' + + +@Adapter.register_lookup +class Endswith(DefaultLookup): + """Exact Contains Query""" + lookup_name = 'endswith' + + +@Adapter.register_lookup +class GreaterThan(DefaultLookup): + """Greater than Query""" + lookup_name = 'gt' + + +@Adapter.register_lookup +class GreaterThanOrEqual(DefaultLookup): + """Greater than or Equal Query""" + lookup_name = 'gte' + + +@Adapter.register_lookup +class LessThan(DefaultLookup): + """Less than Query""" + lookup_name = 'lt' + + +@Adapter.register_lookup +class LessThanOrEqual(DefaultLookup): + """Less than or Equal Query""" + lookup_name = 'lte' + + +@Adapter.register_lookup +class In(DefaultLookup): + """In Query""" + lookup_name = 'in' + + def process_target(self): + """Ensure target is a list or tuple""" + assert type(self.target) in (list, tuple) + return super().process_target() + + +@Adapter.register_lookup +class Overlap(DefaultLookup): + """In Query""" + lookup_name = 'in' + + def process_target(self): + """Ensure target is a list or tuple""" + assert type(self.target) in (list, tuple) + return super().process_target() + + +@Adapter.register_lookup +class Any(DefaultLookup): + """In Query""" + lookup_name = 'in' + + def process_target(self): + """Ensure target is a list or tuple""" + assert type(self.target) in (list, tuple) + return super().process_target() diff --git a/src/protean_sqlalchemy/sa.py b/src/protean_sqlalchemy/sa.py index 6b64ee5..32b6023 100644 --- a/src/protean_sqlalchemy/sa.py +++ b/src/protean_sqlalchemy/sa.py @@ -4,12 +4,14 @@ """ from protean.core import field -from protean.utils.meta import OptionsMeta +from protean.core.repository.base import BaseModelMeta +from protean.core.repository import repo_factory + from sqlalchemy import types as sa_types, Column from sqlalchemy.ext import declarative as sa_dec -class DeclarativeMeta(sa_dec.DeclarativeMeta, OptionsMeta): +class DeclarativeMeta(sa_dec.DeclarativeMeta, BaseModelMeta): """ Metaclass for the Sqlalchemy declarative schema """ field_mapping = { field.Auto: sa_types.Integer, @@ -28,15 +30,25 @@ def __init__(cls, classname, bases, dict_): # Update the class attrs with the entity attributes if cls.__dict__.get('opts_'): entity_cls = cls.__dict__['opts_'].entity_cls - for field_name, field_obj in entity_cls.meta_.\ + for field_name, field_obj in entity_cls.\ declared_fields.items(): # Map the field if not in attributes if field_name not in cls.__dict__: field_cls = type(field_obj) - sa_type_cls = cls.field_mapping.get(field_cls) + if field_cls == field.Reference: + related_ent = repo_factory.get_entity(field_obj.to_cls) + if field_obj.via: + related_attr = getattr( + related_ent, field_obj.via) + else: + related_attr = related_ent.id_field + field_name = field_obj.get_attribute_name() + field_cls = type(related_attr) - # Default to the text type + # Get the SA type and default to the text type if no + # mapping is found + sa_type_cls = cls.field_mapping.get(field_cls) if not sa_type_cls: sa_type_cls = sa_types.String @@ -55,5 +67,4 @@ def __init__(cls, classname, bases, dict_): # Update the attributes of the class setattr(cls, field_name, Column(sa_type_cls(**type_args), **col_args)) - super().__init__(classname, bases, dict_) diff --git a/tests/support/dog.py b/tests/support/dog.py new file mode 100644 index 0000000..53d0f29 --- /dev/null +++ b/tests/support/dog.py @@ -0,0 +1,43 @@ +""" Define entities of the Human Type """ +from protean.core import field +from protean.core.entity import Entity + +from protean_sqlalchemy.repository import SqlalchemyModel + + +class Dog(Entity): + """This is a dummy Dog Entity class""" + name = field.String(required=True, max_length=50, unique=True) + owner = field.String(required=True, max_length=15) + age = field.Integer(default=5) + + def __repr__(self): + return f'' + + +class DogModel(SqlalchemyModel): + """Model for the Dog Entity""" + + class Meta: + """ Meta class for model options""" + entity = Dog + model_name = 'dogs' + + +class RelatedDog(Entity): + """This is a dummy Dog Entity class""" + name = field.String(required=True, max_length=50, unique=True) + owner = field.Reference('RelatedHuman') + age = field.Integer(default=5) + + def __repr__(self): + return f'' + + +class RelatedDogModel(SqlalchemyModel): + """Model for the Dog Entity""" + + class Meta: + """ Meta class for model options""" + entity = RelatedDog + model_name = 'related_dogs' diff --git a/tests/support/human.py b/tests/support/human.py new file mode 100644 index 0000000..6c62eee --- /dev/null +++ b/tests/support/human.py @@ -0,0 +1,55 @@ +""" Define entities of the Human Type """ +from datetime import datetime + +from protean.core import field +from protean.core.entity import Entity +from protean.core.field import association + +from protean_sqlalchemy.repository import SqlalchemyModel + + +class Human(Entity): + """This is a dummy Dog Entity class""" + name = field.StringMedium(required=True, unique=True) + age = field.Integer() + weight = field.Float() + is_married = field.Boolean(default=True) + date_of_birth = field.Date(required=True) + hobbies = field.List() + profile = field.Dict() + address = field.Text() + created_at = field.DateTime(default=datetime.utcnow) + + def __repr__(self): + return f'' + + +class HumanModel(SqlalchemyModel): + """Model for the Human Entity""" + + class Meta: + """ Meta class for model options""" + entity = Human + model_name = 'humans' + bind = 'another_db' + + +class RelatedHuman(Entity): + """This is a dummy Dog Entity class""" + name = field.StringMedium(required=True, unique=True) + age = field.Integer() + weight = field.Float() + date_of_birth = field.Date(required=True) + dogs = association.HasMany('RelatedDog', via='owner_id') + + def __repr__(self): + return f'' + + +class RelatedHumanModel(SqlalchemyModel): + """Model for the Human Entity""" + + class Meta: + """ Meta class for model options""" + entity = RelatedHuman + model_name = 'related_humans' diff --git a/tests/test_filters_lookups.py b/tests/test_filters_lookups.py new file mode 100644 index 0000000..27b7f4a --- /dev/null +++ b/tests/test_filters_lookups.py @@ -0,0 +1,193 @@ +"""Module to test Repository extended functionality """ +from datetime import datetime + +from protean.core.repository import repo_factory +from protean.utils.query import Q + +from protean_sqlalchemy.repository import SqlalchemyModel +from protean_sqlalchemy.utils import drop_tables + +from .support.human import Human +from .support.human import HumanModel + + +class TestFiltersLookups: + """Class to test Sqlalchemy Repository""" + + @classmethod + def setup_class(cls): + """ Setup actions for this test case""" + repo_factory.register(HumanModel) + + # Create all the tables + for conn in repo_factory.connections.values(): + SqlalchemyModel.metadata.create_all(conn.bind) + + # Create the Humans for filtering + cls.humans = [ + Human.create(name='John Doe', age='30', weight='13.45', + date_of_birth='01-01-1989'), + Human.create(name='Jane Doe', age='25', weight='17.45', + date_of_birth='23-08-1994'), + Human.create(name='Greg Manning', age='44', weight='23.45', + date_of_birth='30-07-1975'), + Human.create(name='Red Dread', age='23', weight='33.45', + date_of_birth='12-03-1996') + ] + + @classmethod + def teardown_class(cls): + # Drop all the tables + drop_tables() + + def test_iexact_lookup(self): + """ Test the iexact lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(name__iexact='John doe') + + assert humans is not None + assert humans.total == 1 + + def test_contains_lookup(self): + """ Test the contains lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(name__contains='Doe') + + assert humans is not None + assert humans.total == 2 + + def test_icontains_lookup(self): + """ Test the icontains lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(name__icontains='man') + + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[2].id + + def test_startswith_lookup(self): + """ Test the startswith lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(name__startswith='John') + + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[0].id + + def test_endswith_lookup(self): + """ Test the endswith lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(name__endswith='Doe') + + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[0].id + + def test_gt_lookup(self): + """ Test the gt lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(age__gt=40) + + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[2].id + + def test_gte_lookup(self): + """ Test the gte lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(age__gte=30).order_by(['age']) + + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[0].id + + def test_lt_lookup(self): + """ Test the lt lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(weight__lt=15) + + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[0].id + + def test_lte_lookup(self): + """ Test the lte lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(weight__lte=23.45) + + assert humans is not None + assert humans.total == 3 + assert humans[0].id == self.humans[0].id + + def test_in_lookup(self): + """ Test the lte lookup of the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter(id__in=[self.humans[1].id, + self.humans[3].id]) + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[1].id + + def test_date_lookup(self): + """ Test the lookup of date fields for the Adapter """ + + # Filter the entity and validate the results + humans = Human.query.filter( + date_of_birth__gt='1994-01-01') + + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[1].id + + humans = Human.query.filter( + date_of_birth__lte=datetime(1989, 1, 1).date()) + + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[0].id + + def test_q_filters(self): + """ Test that complex filtering using the Q object""" + + # Filter by 2 conditions + humans = Human.query.filter(Q(name__contains='Doe') & Q(age__gt=28)) + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[0].id + + # Try the same with negation + humans = Human.query.filter(~Q(name__contains='Doe') & Q(age__gt=28)) + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[2].id + + # Try with basic or + humans = Human.query.filter(Q(name__contains='Doe') | Q(age__gt=28)) + assert humans is not None + assert humans.total == 3 + assert humans[0].id == self.humans[0].id + + # Try combination of and and or + humans = Human.query.filter(Q(age__gte=27) | Q(weight__gt=15), + name__contains='Doe') + assert humans is not None + assert humans.total == 2 + assert humans[0].id == self.humans[0].id + + # Try combination of and and or + humans = Human.query.filter( + (Q(weight__lte=20) | (Q(age__gt=30) & Q(name__endswith='Manning'))), + Q(date_of_birth__gt='1994-01-01')) + assert humans is not None + assert humans.total == 1 + assert humans[0].id == self.humans[1].id diff --git a/tests/test_relations.py b/tests/test_relations.py new file mode 100644 index 0000000..5738886 --- /dev/null +++ b/tests/test_relations.py @@ -0,0 +1,75 @@ +"""Module to test Repository extended functionality """ +from protean.core.repository import repo_factory + +from protean_sqlalchemy.utils import create_tables +from protean_sqlalchemy.utils import drop_tables + +from .support.dog import RelatedDog +from .support.dog import RelatedDogModel +from .support.human import RelatedHuman +from .support.human import RelatedHumanModel + + +class TestRelations: + """Class to test Relation field of Sqlalchemy Repository""" + + @classmethod + def setup_class(cls): + """ Setup actions for this test case""" + repo_factory.register(RelatedHumanModel) + repo_factory.register(RelatedDogModel) + + # Save the current connection + cls.conn = repo_factory.connections['default'] + + # Create all the tables + create_tables() + + # Create the Humans for filtering + cls.h1 = RelatedHuman.create( + name='John Doe', age='30', weight='13.45', + date_of_birth='01-01-1989') + cls.h2 = RelatedHuman.create( + name='Greg Manning', age='44', weight='23.45', + date_of_birth='30-07-1975') + + @classmethod + def teardown_class(cls): + # Drop all the tables + drop_tables() + + def test_create_related(self): + """Test Cceating an entity with a related field""" + dog = RelatedDog(name='Jimmy', age=10, owner=self.h1) + dog.save() + + assert dog is not None + assert dog.owner.name == 'John Doe' + + # Check if the object is in the repo + dog_db = self.conn.query(RelatedDogModel).get(dog.id) + assert dog_db is not None + assert dog_db.owner_id == self.h1.id + + def test_update_related(self): + """ Test updating the related field of an entity """ + dog = RelatedDog.query.filter(name='Jimmy').all().first + dog.update(owner=self.h2) + + # Check if the object is in the repo + dog_db = self.conn.query(RelatedDogModel).get(dog.id) + assert dog_db is not None + assert dog_db.owner_id == self.h2.id + + def test_has_many(self): + """ Test getting the has many attribute of Relation""" + # Get the dogs related to the human + assert self.h1.dogs is None + + # Create some dogs + RelatedDog.create(name='Dex', age=6, owner=self.h1) + RelatedDog.create(name='Lord', age=3, owner=self.h1) + + # Get the dogs related to the human + assert self.h1.dogs is not None + assert [d.name for d in self.h1.dogs] == ['Dex', 'Lord'] diff --git a/tests/test_repo_ext.py b/tests/test_repo_ext.py index 1d03a90..6c8bf19 100644 --- a/tests/test_repo_ext.py +++ b/tests/test_repo_ext.py @@ -1,40 +1,14 @@ """Module to test Repository extended functionality """ from datetime import datetime -from protean.core import field -from protean.core.entity import Entity from protean.core.repository import repo_factory from protean_sqlalchemy.repository import SqlalchemyModel -from .test_repository import Dog -from .test_repository import DogModel - - -class Human(Entity): - """This is a dummy Dog Entity class""" - name = field.StringMedium(required=True, unique=True) - age = field.Integer() - weight = field.Float() - is_married = field.Boolean(default=True) - date_of_birth = field.Date(required=True) - hobbies = field.List() - profile = field.Dict() - address = field.Text() - created_at = field.DateTime(default=datetime.utcnow) - - def __repr__(self): - return f'' - - -class HumanModel(SqlalchemyModel): - """Model for the Human Entity""" - - class Meta: - """ Meta class for model options""" - entity = Human - model_name = 'humans' - bind = 'another_db' +from .support.dog import Dog +from .support.dog import DogModel +from .support.human import Human +from .support.human import HumanModel class TestSqlalchemyRepositoryExt: @@ -83,8 +57,8 @@ def test_create(self): def test_multiple_dbs(self): """ Test repository connections to multiple databases""" - humans = Human.filter() + humans = Human.query.filter().all() assert humans is not None - dogs = Dog.filter() + dogs = Dog.query.filter().all() assert dogs is not None diff --git a/tests/test_repository.py b/tests/test_repository.py index 94d376c..624e4dc 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -1,34 +1,15 @@ """Module to test Repository Classes and Functionality""" import pytest from protean.conf import active_config -from protean.core import field -from protean.core.entity import Entity from protean.core.exceptions import ValidationError from protean.core.repository import repo_factory from protean_sqlalchemy.repository import ConnectionHandler -from protean_sqlalchemy.repository import SqlalchemyModel from protean_sqlalchemy.utils import create_tables from protean_sqlalchemy.utils import drop_tables - -class Dog(Entity): - """This is a dummy Dog Entity class""" - name = field.String(required=True, max_length=50, unique=True) - owner = field.String(required=True, max_length=15) - age = field.Integer(default=5) - - def __repr__(self): - return f'' - - -class DogModel(SqlalchemyModel): - """Model for the Dog Entity""" - - class Meta: - """ Meta class for model options""" - entity = Dog - model_name = 'dogs' +from .support.dog import Dog +from .support.dog import DogModel class TestConnectionHandler: @@ -102,7 +83,7 @@ def test_update(self, mocker): """ Test updating an entity in the repository""" # Update the entity and validate the results dog = Dog.get(1) - dog.update(dict(age=7)) + dog.update(age=7) assert dog is not None assert dog.age == 7 @@ -120,21 +101,20 @@ def test_filter(self): Dog.create(name='Gooey', owner='John', age=2) # Filter the entity and validate the results - dogs = Dog.filter(page=1, per_page=15, order_by=['-age'], owner='John') + dogs = Dog.query.filter(owner='John').\ + paginate(page=1, per_page=15).\ + order_by(['-age']).all() + assert dogs is not None assert dogs.total == 3 dog_ages = [d.age for d in dogs.items] assert dog_ages == [10, 7, 2] # Test In and not in query - dogs = Dog.filter(name=['Cash', 'Boxy']) + dogs = Dog.query.filter(name__in=['Cash', 'Boxy']) assert dogs.total == 2 - dogs = Dog.filter(excludes_=dict(name=['Cash', 'Gooey']), owner='John') - assert dogs.total == 1 - - # Test for sql alchemy filter - dogs = Dog.filter(filter_=(DogModel.age > 8)) + dogs = Dog.query.filter(owner='John').exclude(name__in=['Cash', 'Gooey']) assert dogs.total == 1 def test_delete(self): @@ -149,12 +129,6 @@ def test_delete(self): dog_db = self.conn.query(DogModel).filter_by(id=1).first() assert dog_db is None - def test_delete_all(self): - """ Test deleting all entries from the repository""" - # Delete the entity and validate the results - cnt = Dog.filter().total - assert cnt == 3 - def test_close_connection(self): """ Test closing connection to the repository """ repo_factory.close_connections()