Skip to content

Commit

Permalink
Merge branch 'master' of github.com:proteanhq/protean
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Apr 6, 2019
2 parents edefb56 + c5fef3d commit cc3171d
Show file tree
Hide file tree
Showing 9 changed files with 659 additions and 603 deletions.
1,072 changes: 536 additions & 536 deletions src/protean/core/entity.py

Large diffs are not rendered by default.

128 changes: 87 additions & 41 deletions src/protean/core/repository/factory.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
""" Factory class for managing repository connections"""
import logging
from collections import namedtuple
from threading import local

from protean.core.exceptions import ConfigurationError
from protean.core.provider import providers
from protean.utils.generic import fully_qualified_name

logger = logging.getLogger('protean.repository')

Expand All @@ -16,12 +18,16 @@ class RepositoryFactory:
be let go.
"""

# EntityRecord Inner Class, implemented as a namedtuple for ease of use.
# This class will store attributes related to Entity and Models, and will be objects
# in the registry dictionary.
EntityRecord = namedtuple(
'EntityRecord',
'name, qualname, entity_cls, provider_name, model_cls, fully_baked_model')

def __init__(self):
""""Initialize repository factory"""
self._provider_registry = {}
self._entity_registry = {}
self._model_registry = {}
self._fully_baked_models = {}
self._registry = {}
self._connections = local()

def register(self, model_cls, provider_name=None):
Expand All @@ -34,21 +40,66 @@ def register(self, model_cls, provider_name=None):

# Register the model if it does not exist
model_name = model_cls.__name__
entity_name = model_cls.opts_.entity_cls.__name__
entity_name = fully_qualified_name(model_cls.opts_.entity_cls)
provider_name = provider_name or model_cls.opts_.bind or 'default'

if self._provider_registry.get(entity_name):
# This probably is an accidental re-registration of the entity
# and we should warn the user of a possible repository confusion
raise ConfigurationError(
f'Entity {entity_name} has already been registered')
else:
self._provider_registry[entity_name] = provider_name or model_cls.opts_.bind or 'default'
self._model_registry[entity_name] = model_cls
self._entity_registry[entity_name] = model_cls.opts_.entity_cls
try:
entity = self._get_entity_by_class(model_cls.opts_.entity_cls)

if entity:
# This probably is an accidental re-registration of the entity
# and we should warn the user of a possible repository confusion
raise ConfigurationError(
f'Entity {entity_name} has already been registered')
except AssertionError:
# Entity has not been registered yet. Let's go ahead and add it to the registry.
entity_record = RepositoryFactory.EntityRecord(
name=model_cls.opts_.entity_cls.__name__,
qualname=entity_name,
entity_cls=model_cls.opts_.entity_cls,
provider_name=provider_name,
model_cls=model_cls,
fully_baked_model=False
)
self._registry[entity_name] = entity_record
logger.debug(
f'Registered model {model_name} for entity {entity_name} with provider'
f' {provider_name}.')

def _find_entity_in_records_by_class_name(self, entity_name):
"""Fetch by Entity Name in values"""
records = {
key: value for (key, value)
in self._registry.items()
if value.name == entity_name
}
# If more than one record was found, we are dealing with the case of
# an Entity name present in multiple places (packages or plugins). Throw an error
# and ask for a fully qualified Entity name to be specified
if len(records) > 1:
raise ConfigurationError(
f'Entity with name {entity_name} has been registered twice. '
f'Please use fully qualified Entity name to specify the exact Entity.')
elif len(records) == 1:
return next(iter(records.values()))
else:
raise AssertionError(f'No Entity registered with name {entity_name}')

def _get_entity_by_class(self, entity_cls):
"""Fetch Entity record with Entity class details"""
entity_qualname = fully_qualified_name(entity_cls)
if entity_qualname in self._registry:
return self._registry[entity_qualname]
else:
return self._find_entity_in_records_by_class_name(entity_cls.__name__)

def _get_entity_by_name(self, entity_name):
"""Fetch Entity record with an Entity name"""
if entity_name in self._registry:
return self._registry[entity_name]
else:
return self._find_entity_in_records_by_class_name(entity_name)

def _validate_model_cls(self, model_cls):
"""Validate that Model is a valid class"""
# Import here to avoid cyclic dependency
Expand All @@ -58,45 +109,40 @@ def _validate_model_cls(self, model_cls):
raise AssertionError(
f'Model {model_cls} must be subclass of `BaseModel`')

def get_model(self, entity_name):
def get_model(self, entity_cls):
"""Retrieve Model class connected to Entity"""
if entity_name in self._fully_baked_models:
return self._fully_baked_models[entity_name]
entity_record = self._get_entity_by_class(entity_cls)

try:
# This will trigger ``AssertionError`` if entity is not registered
model_cls = self._model_registry[entity_name]

provider = self.get_provider(entity_name)
fully_baked_model = provider.get_model(model_cls)
model_cls = None
if entity_record.fully_baked_model:
model_cls = entity_record.model_cls
else:
provider = self.get_provider(entity_record.provider_name)
baked_model_cls = provider.get_model(entity_record.model_cls)

# Record for future reference
self._fully_baked_models['entity_name'] = fully_baked_model
new_entity_record = entity_record._replace(model_cls=baked_model_cls,
fully_baked_model=True)
self._registry[entity_record.qualname] = new_entity_record

return fully_baked_model
except KeyError:
raise AssertionError(f'No Model registered for {entity_name}')
model_cls = baked_model_cls

return model_cls

def get_entity(self, entity_name):
"""Retrieve Entity class registered by `entity_name`"""
try:
return self._entity_registry[entity_name]
except KeyError:
raise AssertionError(f'No Entity registered with name {entity_name}')
return self._get_entity_by_name(entity_name).entity_cls

def get_provider(self, entity_name):
"""Retrieve the provider name registered for the entity"""
provider_name = self._provider_registry[entity_name]
def get_provider(self, provider_name):
"""Retrieve the provider object with a given provider name"""
return providers.get_provider(provider_name)

def __getattr__(self, entity_name):
try:
provider = self.get_provider(entity_name)
def get_repository(self, entity_cls):
"""Retrieve a Repository for the Model with a live connection"""
entity_record = self._get_entity_by_class(entity_cls)
provider = self.get_provider(entity_record.provider_name)

# Fetch a repository object with live connection
return provider.get_repository(self._model_registry[entity_name])
except KeyError:
raise AssertionError(f'No Model registered for {entity_name}')
return provider.get_repository(entity_record.model_cls)


repo_factory = RepositoryFactory()
5 changes: 5 additions & 0 deletions src/protean/utils/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ def __init__(self, fget):

def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls)


def fully_qualified_name(cls):
"""Return Fully Qualified name along with module"""
return '.'.join([cls.__module__, cls.__qualname__])
39 changes: 22 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,29 @@ def register_models():
def run_around_tests():
"""Cleanup Database after each test run"""
from protean.core.repository import repo_factory
from tests.support.dog import (Dog, RelatedDog, DogRelatedByEmail, HasOneDog1,
HasOneDog2, HasOneDog3, HasManyDog1, HasManyDog2,
HasManyDog3, ThreadedDog)
from tests.support.human import (Human, HasOneHuman1, HasOneHuman2, HasOneHuman3,
HasManyHuman1, HasManyHuman2, HasManyHuman3)

# A test function will be run at this point
yield

repo_factory.Dog.delete_all()
repo_factory.RelatedDog.delete_all()
repo_factory.DogRelatedByEmail.delete_all()
repo_factory.HasOneDog1.delete_all()
repo_factory.HasOneDog2.delete_all()
repo_factory.HasOneDog3.delete_all()
repo_factory.HasManyDog1.delete_all()
repo_factory.HasManyDog2.delete_all()
repo_factory.HasManyDog3.delete_all()
repo_factory.Human.delete_all()
repo_factory.HasOneHuman1.delete_all()
repo_factory.HasOneHuman2.delete_all()
repo_factory.HasOneHuman3.delete_all()
repo_factory.HasManyHuman1.delete_all()
repo_factory.HasManyHuman2.delete_all()
repo_factory.HasManyHuman3.delete_all()
repo_factory.ThreadedDog.delete_all()
repo_factory.get_repository(Dog).delete_all()
repo_factory.get_repository(RelatedDog).delete_all()
repo_factory.get_repository(DogRelatedByEmail).delete_all()
repo_factory.get_repository(HasOneDog1).delete_all()
repo_factory.get_repository(HasOneDog2).delete_all()
repo_factory.get_repository(HasOneDog3).delete_all()
repo_factory.get_repository(HasManyDog1).delete_all()
repo_factory.get_repository(HasManyDog2).delete_all()
repo_factory.get_repository(HasManyDog3).delete_all()
repo_factory.get_repository(Human).delete_all()
repo_factory.get_repository(HasOneHuman1).delete_all()
repo_factory.get_repository(HasOneHuman2).delete_all()
repo_factory.get_repository(HasOneHuman3).delete_all()
repo_factory.get_repository(HasManyHuman1).delete_all()
repo_factory.get_repository(HasManyHuman2).delete_all()
repo_factory.get_repository(HasManyHuman3).delete_all()
repo_factory.get_repository(ThreadedDog).delete_all()
2 changes: 1 addition & 1 deletion tests/core/test_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def test_query_init(self):

assert query is not None
assert isinstance(query, QuerySet)
assert vars(query) == vars(QuerySet('Dog'))
assert vars(query) == vars(QuerySet(Dog))

def test_filter_chain_initialization_from_entity(self):
""" Test that chaining returns a QuerySet for further chaining """
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_list(self):
def test_repr(self):
"""Test that filter is evaluted on calling `list()`"""
query = Dog.query.filter(owner='John').order_by('age')
assert repr(query) == ("<QuerySet: entity: Dog, "
assert repr(query) == ("<QuerySet: entity: <class 'tests.support.dog.Dog'>, "
"criteria: ('protean.utils.query.Q', (), {'owner': 'John'}), "
"page: 1, "
"per_page: 10, order_by: {'age'}>")
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_init(self):
"""Test successful access to the Dog repository"""

Dog.query.all()
current_db = dict(repo_factory.Dog.conn)
current_db = dict(repo_factory.get_repository(Dog).conn)
assert current_db['data'] == {'dogs': {}}

def test_create_error(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/support/dog.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class RelatedDog2(Entity):
"""
name = field.String(required=True, unique=True, max_length=50)
age = field.Integer(default=5)
owner = field.Reference('Human')
owner = field.Reference('tests.support.human.Human')


class RelatedDog2Model(DictModel):
Expand Down
10 changes: 5 additions & 5 deletions tests/support/human.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class HasOneHuman1(Entity):
first_name = field.String(required=True, unique=True, max_length=50)
last_name = field.String(required=True, unique=True, max_length=50)
email = field.String(required=True, unique=True, max_length=50)
dog = association.HasOne('HasOneDog1')
dog = association.HasOne('tests.support.dog.HasOneDog1')


class HasOneHuman1Model(DictModel):
Expand All @@ -46,7 +46,7 @@ class HasOneHuman2(Entity):
first_name = field.String(required=True, unique=True, max_length=50)
last_name = field.String(required=True, unique=True, max_length=50)
email = field.String(required=True, unique=True, max_length=50)
dog = association.HasOne('HasOneDog2', via='human_id')
dog = association.HasOne('tests.support.dog.HasOneDog2', via='human_id')


class HasOneHuman2Model(DictModel):
Expand All @@ -65,7 +65,7 @@ class HasOneHuman3(Entity):
first_name = field.String(required=True, unique=True, max_length=50)
last_name = field.String(required=True, unique=True, max_length=50)
email = field.String(required=True, unique=True, max_length=50)
dog = association.HasOne('HasOneDog3', via='human_id')
dog = association.HasOne('tests.support.dog.HasOneDog3', via='human_id')


class HasOneHuman3Model(DictModel):
Expand All @@ -82,7 +82,7 @@ class HasManyHuman1(Entity):
first_name = field.String(required=True, unique=True, max_length=50)
last_name = field.String(required=True, unique=True, max_length=50)
email = field.String(required=True, unique=True, max_length=50)
dogs = association.HasMany('HasManyDog1')
dogs = association.HasMany('tests.support.dog.HasManyDog1')


class HasManyHuman1Model(DictModel):
Expand Down Expand Up @@ -120,7 +120,7 @@ class HasManyHuman3(Entity):
first_name = field.String(required=True, unique=True, max_length=50)
last_name = field.String(required=True, unique=True, max_length=50)
email = field.String(required=True, unique=True, max_length=50)
dogs = association.HasMany('HasManyDog3', via='human_id')
dogs = association.HasMany('tests.support.dog.HasManyDog3', via='human_id')


class HasManyHuman3Model(DictModel):
Expand Down

0 comments on commit cc3171d

Please sign in to comment.