Module jumpscale.core.base.factory

Hierarchal configurations and factories for any Base type/class.

It is implemented using:

  • Factory: the base for all factories, implements an in-memory factory for any type.
  • StoredFactory: a factory which stores and load configuration according to the current configured store backend.

Default store configured is file system, you can show current store config by doing:

✗ jsctl config get store
store = filesystem

Get all stores configurations:

✗ jsctl config get stores
redis.hostname = "localhost"
redis.port = 6379
filesystem.path = "/home/abom/.config/jumpscale/secureconfig"
whoosh.path = "/home/abom/.config/jumpscale/whoosh_indexes"

For example, set current store to redis:

jsctl config update store redis

This will use current redis config (hostname: localhost, port: 6379).

Expand source code
"""
Hierarchal configurations and factories for any `jumpscale.core.base.meta.Base` type/class.

It is implemented using:

- `Factory`: the base for all factories, implements an in-memory factory for any type.
- `StoredFactory`: a factory which stores and load configuration according to the current configured store backend.


Default store configured is file system, you can show current store config by doing:

```
✗ jsctl config get store
store = filesystem
```

Get all stores configurations:

```
✗ jsctl config get stores
redis.hostname = "localhost"
redis.port = 6379
filesystem.path = "/home/abom/.config/jumpscale/secureconfig"
whoosh.path = "/home/abom/.config/jumpscale/whoosh_indexes"
```

For example, set current store to redis:

```
jsctl config update store redis
```

This will use current redis config (hostname: localhost, port: 6379).
"""
from functools import partial
from jumpscale.core import config, events

from .events import InstanceCreateEvent, InstanceDeleteEvent
from .store import ConfigNotFound, KEY_FIELD_NAME, Location
from .store.filesystem import FileSystemStore
from .store.redis import RedisStore
from .store.whooshfts import WhooshStore


STORES = {"filesystem": FileSystemStore, "redis": RedisStore, "whoosh": WhooshStore}


class DuplicateError(Exception):
    """
    raised when you try to create an instance by the same name of an existing one for a factory
    """


class Factory:
    """
    Base factory, where you can create/get/list/delete new instances.

    All of the operations are done in memory, also instances will be available as properties.

    Example:

    ```python

    class Car(Base):
        name = fields.String()
        color = fields.String()

    cars = Factory(Car)
    acar = cars.get("mycar")
    acar.name = "fiat"

    print(cars.mycar.name)
    ```
    """

    def __init__(self, type_, name_=None, parent_instance_=None, parent_factory_=None):
        """
        get a new factory given the type to create instances for.

        Any factory can have a name, parent `Base` instance and a parent factory.

        Args:
            type_ (Base): `Base` class type
            name_ (str, optional): factory name. Defaults to None.
            parent_instance_ (Base, optional): a parent `Base` instance. Defaults to None.
            parent_factory_ (Factory, optional): a parent `Factory`. Defaults to None.
        """
        self.__name = name_
        self.__parent_instance = parent_instance_
        self.__parent_factory = parent_factory_

        self.type = type_
        self.count = 0

    @property
    def parent_instance(self):
        return self.__parent_instance

    @property
    def name(self):
        return self.__name

    @property
    def parent_factory(self):
        return self.__parent_factory

    def _set_parent_factory(self, factory):
        """
        set current parent factory

        Args:
            factory (Factory)
        """
        self.__parent_factory = factory

    def find(self, name):
        """
        find an instance with the given name

        Args:
            name (str): instance name

        Raises:
            ValueError: in case the name is an internal attribute of this factory, like `get` or `new`.

        Returns:
            Base or NoneType: an instance or none
        """
        instance = getattr(self, name, None)
        if instance and not isinstance(instance, self.type):
            raise ValueError("{} is an internal attribute".format(name))
        return instance

    def _create_instance(self, name_, *args, **kwargs):
        """
        create a new instance, this method is only responsible for:

        - validating the name used is correct (must be a valid identifier and does not start with "__")
        - creating the instance from `self.type`
        - setting this instance name with the given name
        - setting the parent instance to it if this factory have a parent instance
        - update the counter and trigger `_created`

        Args:
            name_ (str): instance name

        Raises:
            ValueError: in case the name is not a valid identifier (e.g contains spaces) or starts with "__"

        Returns:
            Base: instance
        """
        if not name_.isidentifier():
            raise ValueError("{} is not a valid identifier".format(name_))

        if name_.startswith("__"):
            raise ValueError("name cannot start with '__'")

        kwargs["instance_name_"] = name_
        kwargs["parent_"] = self.parent_instance
        instance = self.type(*args, **kwargs)

        self.count += 1
        self._created(instance)
        return instance

    def new(self, name, *args, **kwargs):
        """
        get a new instance and make it available as an attribute

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        if self.find(name):
            raise DuplicateError(f"instance with name {name} already exists")

        instance = self._create_instance(name, *args, **kwargs)
        setattr(self, name, instance)
        return instance

    def get(self, name, *args, **kwargs):
        """
        get an instance (will create if it does not exist)

        if the instance with `name` exists, `args` and `kwargs` are ignored.

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        instance = self.find(name)
        if instance:
            return instance
        return self.new(name, *args, **kwargs)

    def _delete_instance(self, name):
        """
        delete the instance with given name

        here, we only remove the attribute

        Args:
            name (str): instance/attribute name
        """
        if hasattr(self, name):
            delattr(self, name)

    def delete(self, name):
        """
        delete an instance (with its attribute)

        this will update the count and trigger `_deleted`

        Args:
            name (str)
        """
        self.count -= 1
        self._delete_instance(name)
        self._deleted(name)

    def _deleted(self, name):
        """
        called when an instance is deleted, will trigger `InstanceDeleteEvent`

        Args:
            name (name)
        """
        event = InstanceDeleteEvent(name, factory=self)
        events.notify(event)

    def _created(self, instance):
        """
        called when an instance is created, will trigger `InstanceCreateEvent`

        Args:
            instance (Base)
        """
        event = InstanceCreateEvent(instance=instance, factory=self)
        events.notify(event)

    def list_all(self):
        """
        get a set of all instance names

        this only get a list of all properties that are of factory `Base` type.

        Returns:
            set
        """
        names = set()
        for name, value in self.__dict__.items():
            if isinstance(value, self.type) and not name.startswith("__"):
                names.add(name)
        return names


class StoredFactory(events.Handler, Factory):
    """
    Stored factories are a custom type of `Factory`, which uses current configured store backend
    to store all instance configurations.
    """

    STORE = STORES[config.get("store")]

    def __init__(self, type_, name_=None, parent_instance_=None, parent_factory_=None):
        """
        get a new stored factory given the type to create and store instances for.

        Any factory can have a name, parent `Base` instance and a parent factory.

        Once a stored factory is created, it tries to lazy-load all current configuration for given `type_`.

        Args:
            type_ (Base): `Base` class type
            name_ (str, optional): factory name. Defaults to None.
            parent_instance_ (Base, optional): a parent `Base` instance. Defaults to None.
            parent_factory_ (Factory, optional): a parent `Factory`. Defaults to None.
        """
        super().__init__(type_, name_=name_, parent_instance_=parent_instance_, parent_factory_=parent_factory_)
        self.__store = None

        if not parent_instance_:
            # no parent, then load all instance configurations
            # if it's a parent, it should trigger this loading
            self._load()

        # to handle when a parent instance is deleted
        events.add_listenter(self, InstanceDeleteEvent)

        # if we need to always reload the config from store when getting an instance
        self.always_reload = config.get("factory").get("always_reload", False)

    @property
    def parent_location(self):
        if not self.parent_factory:
            raise ValueError("cannot get parent location if parent factory is not set")
        return self.parent_factory.location

    @property
    def location(self):
        """
        get a unique location for this factory

        Returns:
            Location: location object
        """
        name_list = []

        # first, get the location of parent factory if any
        if self.parent_factory:
            name_list += self.parent_location.name_list

        # if we have a parent instance, then this location should be unique
        # for this instance
        if self.parent_instance:
            name_list.append(self.parent_instance.instance_name)

        # if this factory have a name, append it too
        if self.name:
            name_list.append(self.name)

        # then we append the location of the type
        type_location = Location.from_type(self.type)
        name_list += type_location.name_list

        return Location(*name_list, type_=self.type)

    @property
    def store(self):
        if not self.__store:
            self.__store = self.STORE(self.location)
        return self.__store

    def _validate_and_save_instance(self, instance):
        """
        validate and save a given instance to the store

        Args:
            instance (Base)
        """
        instance.validate()
        self.store.save(instance.instance_name, instance._get_data())
        if instance.parent and hasattr(instance.parent, "save"):
            instance.parent.save()

    def handle(self, ev):
        """
        handle when the parent instance is deleted

        Args:
            ev (InstanceDeleteEvent): instance delete event
        """
        # do nothing if this is a root factory
        if not self.parent_instance or not self.parent_factory:
            return

        # handle deletion of children
        if isinstance(ev, InstanceDeleteEvent):
            if ev.factory == self.parent_factory and ev.name == self.parent_instance.instance_name:
                for name in self.list_all():
                    self.delete(name)

    def _load_sub_factories(self, instance):
        """
        load sub-factories for an instance

        this will also set current factory as a parent for any sub-factories this instance have

        Args:
            instance (Base)
        """
        for factory in instance._get_factories().values():
            factory._set_parent_factory(self)
            factory._load()

    def _init_save_and_sub_factories(self, instance):
        """
        allow an instance to have a `save` method

        also, this method inits the loading of sub-factories for this instance

        Args:
            instance (Base)
        """
        instance.save = partial(self._validate_and_save_instance, instance)
        self._load_sub_factories(instance)

    def _get_object_from_config(self, name, data):
        instance = self._create_instance(name, **data)
        self._init_save_and_sub_factories(instance)
        return instance

    def _load_from_store(self, name):
        """
        loads instance from store

        Args:
            name (str): instance name
        Returns:
            Base or NoneType: an instance or none
        """
        try:
            instance_config = self.store.get(name)
        except ConfigNotFound:
            return

        instance = self._get_object_from_config(name, instance_config)
        setattr(self, name, instance)
        return instance

    def find(self, name):
        """
        find an instance with the given name

        Args:
            name (str): instance name

        Raises:
            ValueError: in case the name is an internal attribute of this factory, like `get` or `new`.

        Returns:
            Base or NoneType: an instance or none
        """
        instance = super().find(name)
        if instance:
            if self.always_reload:
                try:
                    instance._set_data(self.store.get(name))
                except ConfigNotFound:
                    # no config is written, still not saved
                    pass
        else:
            instance = self._load_from_store(name)

        return instance

    def new(self, name, *args, **kwargs):
        """
        get a new instance and make it available as an attribute

        this method also initialize sub-factories for this instance.

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        instance = super().new(name, *args, **kwargs)
        self._init_save_and_sub_factories(instance)
        return instance

    def get_instance_property(self, name):
        """
        return a new property descriptor for a given name, this will help in lazy-loading
        as this property will only create and load an instance from the store once accessed.

        Args:
            name (str): instance name

        Returns:
            property: property descriptor (object)
        """
        inner_name = f"__{name}"

        def getter(factory):
            # we need to avoid AttributeError which hasattr swallows
            # instead we check the internal __dict__
            if inner_name in factory.__dict__:
                return getattr(factory, inner_name)

            instance = self._get_object_from_config(name, factory.store.get(name))
            setattr(factory, inner_name, instance)
            return instance

        return property(getter)

    def _load(self):
        """
        lazy-load all instance configuration

        it will create a new class for this factory with property descriptors for all instances listed by the store
        """
        # get a new class based on self.__class__
        new_cls = type(self.__class__.__name__, (self.__class__,), {})

        # list all instance names in the store and set a property descriptor as an attribute
        for name in self.store.list_all():
            # it will create the actual instance with configuration from the store when accessed only
            setattr(new_cls, name, self.get_instance_property(name))

        # update current factory class with this class
        self.__class__ = new_cls

    def _delete_instance(self, name):
        """
        delete an instance

        this will delete it from the store too, also, delete its property descriptor if found

        Args:
            name (str): instance name
        """
        self.store.delete(name)

        class_prop = getattr(self.__class__, name, None)
        if isinstance(class_prop, property):
            # created with a property descriptor inside the class, delete it
            delattr(self.__class__, name)

            # check if the instance was actually created or not (if the property was accessed)
            inner_name = f"__{name}"
            if hasattr(self, inner_name):
                # delete the instance itself too if it was accessed and created
                delattr(self, inner_name)
        else:
            super()._delete_instance(name)

    def find_many(self, cursor_=None, limit_=None, **query):
        """
        do a search against the store (not loaded objects) with this query.

        queries can relate to current store backend used.

        Keyword Args:
            cursor_ (any, optional): an optional cursor, to start searching from. Defaults to None.
            limit_ (int, optional): results limit. Defaults to None.
            query: a mapping for field/query, e.g. first_name="aa"

        Returns:
            tuple: the new cursor, total results count, and a list of objects as a result
        """
        if not query:
            raise ValueError("at least one query parameter is required, e.g. age=10")

        new_cursor, count, result = self.store.find(cursor_=cursor_, limit_=limit_, **query)
        return (new_cursor, count, (self._get_object_from_config(data[KEY_FIELD_NAME], data) for data in result))

    def list_all(self):
        """
        get all instance names (stored or not)

        Returns:
            set: instance names
        """
        names = set(self.store.list_all())
        return names.union(super().list_all())

    def __iter__(self):
        for value in vars(self).values():
            if isinstance(value, self.type):
                yield value

    def __eq__(self, other):
        return self.location == other.location

    def __hash__(self):
        return hash(self.location)

    def __str__(self):
        """
        readale string for this factory

        Returns:
            str
        """
        return f"{self.__class__.__name__}({self.type.__name__})"

    __repr__ = __str__

Classes

class DuplicateError (*args, **kwargs)

raised when you try to create an instance by the same name of an existing one for a factory

Expand source code
class DuplicateError(Exception):
    """
    raised when you try to create an instance by the same name of an existing one for a factory
    """

Ancestors

  • builtins.Exception
  • builtins.BaseException
class Factory (type_, name_=None, parent_instance_=None, parent_factory_=None)

Base factory, where you can create/get/list/delete new instances.

All of the operations are done in memory, also instances will be available as properties.

Example:


class Car(Base):
    name = fields.String()
    color = fields.String()

cars = Factory(Car)
acar = cars.get("mycar")
acar.name = "fiat"

print(cars.mycar.name)

get a new factory given the type to create instances for.

Any factory can have a name, parent Base instance and a parent factory.

Args

type_ : Base
Base class type
name_ : str, optional
factory name. Defaults to None.
parent_instance_ : Base, optional
a parent Base instance. Defaults to None.
parent_factory_ : Factory, optional
a parent Factory. Defaults to None.
Expand source code
class Factory:
    """
    Base factory, where you can create/get/list/delete new instances.

    All of the operations are done in memory, also instances will be available as properties.

    Example:

    ```python

    class Car(Base):
        name = fields.String()
        color = fields.String()

    cars = Factory(Car)
    acar = cars.get("mycar")
    acar.name = "fiat"

    print(cars.mycar.name)
    ```
    """

    def __init__(self, type_, name_=None, parent_instance_=None, parent_factory_=None):
        """
        get a new factory given the type to create instances for.

        Any factory can have a name, parent `Base` instance and a parent factory.

        Args:
            type_ (Base): `Base` class type
            name_ (str, optional): factory name. Defaults to None.
            parent_instance_ (Base, optional): a parent `Base` instance. Defaults to None.
            parent_factory_ (Factory, optional): a parent `Factory`. Defaults to None.
        """
        self.__name = name_
        self.__parent_instance = parent_instance_
        self.__parent_factory = parent_factory_

        self.type = type_
        self.count = 0

    @property
    def parent_instance(self):
        return self.__parent_instance

    @property
    def name(self):
        return self.__name

    @property
    def parent_factory(self):
        return self.__parent_factory

    def _set_parent_factory(self, factory):
        """
        set current parent factory

        Args:
            factory (Factory)
        """
        self.__parent_factory = factory

    def find(self, name):
        """
        find an instance with the given name

        Args:
            name (str): instance name

        Raises:
            ValueError: in case the name is an internal attribute of this factory, like `get` or `new`.

        Returns:
            Base or NoneType: an instance or none
        """
        instance = getattr(self, name, None)
        if instance and not isinstance(instance, self.type):
            raise ValueError("{} is an internal attribute".format(name))
        return instance

    def _create_instance(self, name_, *args, **kwargs):
        """
        create a new instance, this method is only responsible for:

        - validating the name used is correct (must be a valid identifier and does not start with "__")
        - creating the instance from `self.type`
        - setting this instance name with the given name
        - setting the parent instance to it if this factory have a parent instance
        - update the counter and trigger `_created`

        Args:
            name_ (str): instance name

        Raises:
            ValueError: in case the name is not a valid identifier (e.g contains spaces) or starts with "__"

        Returns:
            Base: instance
        """
        if not name_.isidentifier():
            raise ValueError("{} is not a valid identifier".format(name_))

        if name_.startswith("__"):
            raise ValueError("name cannot start with '__'")

        kwargs["instance_name_"] = name_
        kwargs["parent_"] = self.parent_instance
        instance = self.type(*args, **kwargs)

        self.count += 1
        self._created(instance)
        return instance

    def new(self, name, *args, **kwargs):
        """
        get a new instance and make it available as an attribute

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        if self.find(name):
            raise DuplicateError(f"instance with name {name} already exists")

        instance = self._create_instance(name, *args, **kwargs)
        setattr(self, name, instance)
        return instance

    def get(self, name, *args, **kwargs):
        """
        get an instance (will create if it does not exist)

        if the instance with `name` exists, `args` and `kwargs` are ignored.

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        instance = self.find(name)
        if instance:
            return instance
        return self.new(name, *args, **kwargs)

    def _delete_instance(self, name):
        """
        delete the instance with given name

        here, we only remove the attribute

        Args:
            name (str): instance/attribute name
        """
        if hasattr(self, name):
            delattr(self, name)

    def delete(self, name):
        """
        delete an instance (with its attribute)

        this will update the count and trigger `_deleted`

        Args:
            name (str)
        """
        self.count -= 1
        self._delete_instance(name)
        self._deleted(name)

    def _deleted(self, name):
        """
        called when an instance is deleted, will trigger `InstanceDeleteEvent`

        Args:
            name (name)
        """
        event = InstanceDeleteEvent(name, factory=self)
        events.notify(event)

    def _created(self, instance):
        """
        called when an instance is created, will trigger `InstanceCreateEvent`

        Args:
            instance (Base)
        """
        event = InstanceCreateEvent(instance=instance, factory=self)
        events.notify(event)

    def list_all(self):
        """
        get a set of all instance names

        this only get a list of all properties that are of factory `Base` type.

        Returns:
            set
        """
        names = set()
        for name, value in self.__dict__.items():
            if isinstance(value, self.type) and not name.startswith("__"):
                names.add(name)
        return names

Subclasses

Instance variables

var name
Expand source code
@property
def name(self):
    return self.__name
var parent_factory
Expand source code
@property
def parent_factory(self):
    return self.__parent_factory
var parent_instance
Expand source code
@property
def parent_instance(self):
    return self.__parent_instance

Methods

def delete(self, name)

delete an instance (with its attribute)

this will update the count and trigger _deleted

Args

name (str)

Expand source code
def delete(self, name):
    """
    delete an instance (with its attribute)

    this will update the count and trigger `_deleted`

    Args:
        name (str)
    """
    self.count -= 1
    self._delete_instance(name)
    self._deleted(name)
def find(self, name)

find an instance with the given name

Args

name : str
instance name

Raises

ValueError
in case the name is an internal attribute of this factory, like get or new.

Returns

Base or NoneType
an instance or none
Expand source code
def find(self, name):
    """
    find an instance with the given name

    Args:
        name (str): instance name

    Raises:
        ValueError: in case the name is an internal attribute of this factory, like `get` or `new`.

    Returns:
        Base or NoneType: an instance or none
    """
    instance = getattr(self, name, None)
    if instance and not isinstance(instance, self.type):
        raise ValueError("{} is an internal attribute".format(name))
    return instance
def get(self, name, *args, **kwargs)

get an instance (will create if it does not exist)

if the instance with name exists, args and kwargs are ignored.

Args

name : str
name
*args
arbitrary arguments passed to the factory Base type
*kwargs
arbitrary keyword arguments passed to the factory Base type

Raises

DuplicateError
in case an instance with the same name exists

Returns

Base
instance
Expand source code
def get(self, name, *args, **kwargs):
    """
    get an instance (will create if it does not exist)

    if the instance with `name` exists, `args` and `kwargs` are ignored.

    Args:
        name (str): name
        *args: arbitrary arguments passed to the factory `Base` type
        *kwargs: arbitrary keyword arguments passed to the factory `Base` type

    Raises:
        DuplicateError: in case an instance with the same name exists

    Returns:
        Base: instance
    """
    instance = self.find(name)
    if instance:
        return instance
    return self.new(name, *args, **kwargs)
def list_all(self)

get a set of all instance names

this only get a list of all properties that are of factory Base type.

Returns

set

Expand source code
def list_all(self):
    """
    get a set of all instance names

    this only get a list of all properties that are of factory `Base` type.

    Returns:
        set
    """
    names = set()
    for name, value in self.__dict__.items():
        if isinstance(value, self.type) and not name.startswith("__"):
            names.add(name)
    return names
def new(self, name, *args, **kwargs)

get a new instance and make it available as an attribute

Args

name : str
name
*args
arbitrary arguments passed to the factory Base type
*kwargs
arbitrary keyword arguments passed to the factory Base type

Raises

DuplicateError
in case an instance with the same name exists

Returns

Base
instance
Expand source code
def new(self, name, *args, **kwargs):
    """
    get a new instance and make it available as an attribute

    Args:
        name (str): name
        *args: arbitrary arguments passed to the factory `Base` type
        *kwargs: arbitrary keyword arguments passed to the factory `Base` type

    Raises:
        DuplicateError: in case an instance with the same name exists

    Returns:
        Base: instance
    """
    if self.find(name):
        raise DuplicateError(f"instance with name {name} already exists")

    instance = self._create_instance(name, *args, **kwargs)
    setattr(self, name, instance)
    return instance
class StoredFactory (type_, name_=None, parent_instance_=None, parent_factory_=None)

Stored factories are a custom type of Factory, which uses current configured store backend to store all instance configurations.

get a new stored factory given the type to create and store instances for.

Any factory can have a name, parent Base instance and a parent factory.

Once a stored factory is created, it tries to lazy-load all current configuration for given type_.

Args

type_ : Base
Base class type
name_ : str, optional
factory name. Defaults to None.
parent_instance_ : Base, optional
a parent Base instance. Defaults to None.
parent_factory_ : Factory, optional
a parent Factory. Defaults to None.
Expand source code
class StoredFactory(events.Handler, Factory):
    """
    Stored factories are a custom type of `Factory`, which uses current configured store backend
    to store all instance configurations.
    """

    STORE = STORES[config.get("store")]

    def __init__(self, type_, name_=None, parent_instance_=None, parent_factory_=None):
        """
        get a new stored factory given the type to create and store instances for.

        Any factory can have a name, parent `Base` instance and a parent factory.

        Once a stored factory is created, it tries to lazy-load all current configuration for given `type_`.

        Args:
            type_ (Base): `Base` class type
            name_ (str, optional): factory name. Defaults to None.
            parent_instance_ (Base, optional): a parent `Base` instance. Defaults to None.
            parent_factory_ (Factory, optional): a parent `Factory`. Defaults to None.
        """
        super().__init__(type_, name_=name_, parent_instance_=parent_instance_, parent_factory_=parent_factory_)
        self.__store = None

        if not parent_instance_:
            # no parent, then load all instance configurations
            # if it's a parent, it should trigger this loading
            self._load()

        # to handle when a parent instance is deleted
        events.add_listenter(self, InstanceDeleteEvent)

        # if we need to always reload the config from store when getting an instance
        self.always_reload = config.get("factory").get("always_reload", False)

    @property
    def parent_location(self):
        if not self.parent_factory:
            raise ValueError("cannot get parent location if parent factory is not set")
        return self.parent_factory.location

    @property
    def location(self):
        """
        get a unique location for this factory

        Returns:
            Location: location object
        """
        name_list = []

        # first, get the location of parent factory if any
        if self.parent_factory:
            name_list += self.parent_location.name_list

        # if we have a parent instance, then this location should be unique
        # for this instance
        if self.parent_instance:
            name_list.append(self.parent_instance.instance_name)

        # if this factory have a name, append it too
        if self.name:
            name_list.append(self.name)

        # then we append the location of the type
        type_location = Location.from_type(self.type)
        name_list += type_location.name_list

        return Location(*name_list, type_=self.type)

    @property
    def store(self):
        if not self.__store:
            self.__store = self.STORE(self.location)
        return self.__store

    def _validate_and_save_instance(self, instance):
        """
        validate and save a given instance to the store

        Args:
            instance (Base)
        """
        instance.validate()
        self.store.save(instance.instance_name, instance._get_data())
        if instance.parent and hasattr(instance.parent, "save"):
            instance.parent.save()

    def handle(self, ev):
        """
        handle when the parent instance is deleted

        Args:
            ev (InstanceDeleteEvent): instance delete event
        """
        # do nothing if this is a root factory
        if not self.parent_instance or not self.parent_factory:
            return

        # handle deletion of children
        if isinstance(ev, InstanceDeleteEvent):
            if ev.factory == self.parent_factory and ev.name == self.parent_instance.instance_name:
                for name in self.list_all():
                    self.delete(name)

    def _load_sub_factories(self, instance):
        """
        load sub-factories for an instance

        this will also set current factory as a parent for any sub-factories this instance have

        Args:
            instance (Base)
        """
        for factory in instance._get_factories().values():
            factory._set_parent_factory(self)
            factory._load()

    def _init_save_and_sub_factories(self, instance):
        """
        allow an instance to have a `save` method

        also, this method inits the loading of sub-factories for this instance

        Args:
            instance (Base)
        """
        instance.save = partial(self._validate_and_save_instance, instance)
        self._load_sub_factories(instance)

    def _get_object_from_config(self, name, data):
        instance = self._create_instance(name, **data)
        self._init_save_and_sub_factories(instance)
        return instance

    def _load_from_store(self, name):
        """
        loads instance from store

        Args:
            name (str): instance name
        Returns:
            Base or NoneType: an instance or none
        """
        try:
            instance_config = self.store.get(name)
        except ConfigNotFound:
            return

        instance = self._get_object_from_config(name, instance_config)
        setattr(self, name, instance)
        return instance

    def find(self, name):
        """
        find an instance with the given name

        Args:
            name (str): instance name

        Raises:
            ValueError: in case the name is an internal attribute of this factory, like `get` or `new`.

        Returns:
            Base or NoneType: an instance or none
        """
        instance = super().find(name)
        if instance:
            if self.always_reload:
                try:
                    instance._set_data(self.store.get(name))
                except ConfigNotFound:
                    # no config is written, still not saved
                    pass
        else:
            instance = self._load_from_store(name)

        return instance

    def new(self, name, *args, **kwargs):
        """
        get a new instance and make it available as an attribute

        this method also initialize sub-factories for this instance.

        Args:
            name (str): name
            *args: arbitrary arguments passed to the factory `Base` type
            *kwargs: arbitrary keyword arguments passed to the factory `Base` type

        Raises:
            DuplicateError: in case an instance with the same name exists

        Returns:
            Base: instance
        """
        instance = super().new(name, *args, **kwargs)
        self._init_save_and_sub_factories(instance)
        return instance

    def get_instance_property(self, name):
        """
        return a new property descriptor for a given name, this will help in lazy-loading
        as this property will only create and load an instance from the store once accessed.

        Args:
            name (str): instance name

        Returns:
            property: property descriptor (object)
        """
        inner_name = f"__{name}"

        def getter(factory):
            # we need to avoid AttributeError which hasattr swallows
            # instead we check the internal __dict__
            if inner_name in factory.__dict__:
                return getattr(factory, inner_name)

            instance = self._get_object_from_config(name, factory.store.get(name))
            setattr(factory, inner_name, instance)
            return instance

        return property(getter)

    def _load(self):
        """
        lazy-load all instance configuration

        it will create a new class for this factory with property descriptors for all instances listed by the store
        """
        # get a new class based on self.__class__
        new_cls = type(self.__class__.__name__, (self.__class__,), {})

        # list all instance names in the store and set a property descriptor as an attribute
        for name in self.store.list_all():
            # it will create the actual instance with configuration from the store when accessed only
            setattr(new_cls, name, self.get_instance_property(name))

        # update current factory class with this class
        self.__class__ = new_cls

    def _delete_instance(self, name):
        """
        delete an instance

        this will delete it from the store too, also, delete its property descriptor if found

        Args:
            name (str): instance name
        """
        self.store.delete(name)

        class_prop = getattr(self.__class__, name, None)
        if isinstance(class_prop, property):
            # created with a property descriptor inside the class, delete it
            delattr(self.__class__, name)

            # check if the instance was actually created or not (if the property was accessed)
            inner_name = f"__{name}"
            if hasattr(self, inner_name):
                # delete the instance itself too if it was accessed and created
                delattr(self, inner_name)
        else:
            super()._delete_instance(name)

    def find_many(self, cursor_=None, limit_=None, **query):
        """
        do a search against the store (not loaded objects) with this query.

        queries can relate to current store backend used.

        Keyword Args:
            cursor_ (any, optional): an optional cursor, to start searching from. Defaults to None.
            limit_ (int, optional): results limit. Defaults to None.
            query: a mapping for field/query, e.g. first_name="aa"

        Returns:
            tuple: the new cursor, total results count, and a list of objects as a result
        """
        if not query:
            raise ValueError("at least one query parameter is required, e.g. age=10")

        new_cursor, count, result = self.store.find(cursor_=cursor_, limit_=limit_, **query)
        return (new_cursor, count, (self._get_object_from_config(data[KEY_FIELD_NAME], data) for data in result))

    def list_all(self):
        """
        get all instance names (stored or not)

        Returns:
            set: instance names
        """
        names = set(self.store.list_all())
        return names.union(super().list_all())

    def __iter__(self):
        for value in vars(self).values():
            if isinstance(value, self.type):
                yield value

    def __eq__(self, other):
        return self.location == other.location

    def __hash__(self):
        return hash(self.location)

    def __str__(self):
        """
        readale string for this factory

        Returns:
            str
        """
        return f"{self.__class__.__name__}({self.type.__name__})"

    __repr__ = __str__

Ancestors

Subclasses

Class variables

var STORE

Filesystem store is an EncryptedConfigStore

It saves the config relative to config_env.get_store_config("filesystem")

To store every instance config in a different path, it uses the given Location.

Instance variables

var location

get a unique location for this factory

Returns

Location
location object
Expand source code
@property
def location(self):
    """
    get a unique location for this factory

    Returns:
        Location: location object
    """
    name_list = []

    # first, get the location of parent factory if any
    if self.parent_factory:
        name_list += self.parent_location.name_list

    # if we have a parent instance, then this location should be unique
    # for this instance
    if self.parent_instance:
        name_list.append(self.parent_instance.instance_name)

    # if this factory have a name, append it too
    if self.name:
        name_list.append(self.name)

    # then we append the location of the type
    type_location = Location.from_type(self.type)
    name_list += type_location.name_list

    return Location(*name_list, type_=self.type)
var parent_location
Expand source code
@property
def parent_location(self):
    if not self.parent_factory:
        raise ValueError("cannot get parent location if parent factory is not set")
    return self.parent_factory.location
var store
Expand source code
@property
def store(self):
    if not self.__store:
        self.__store = self.STORE(self.location)
    return self.__store

Methods

def find_many(self, cursor_=None, limit_=None, **query)

do a search against the store (not loaded objects) with this query.

queries can relate to current store backend used.

Keyword Args: cursor_ (any, optional): an optional cursor, to start searching from. Defaults to None. limit_ (int, optional): results limit. Defaults to None. query: a mapping for field/query, e.g. first_name="aa"

Returns

tuple
the new cursor, total results count, and a list of objects as a result
Expand source code
def find_many(self, cursor_=None, limit_=None, **query):
    """
    do a search against the store (not loaded objects) with this query.

    queries can relate to current store backend used.

    Keyword Args:
        cursor_ (any, optional): an optional cursor, to start searching from. Defaults to None.
        limit_ (int, optional): results limit. Defaults to None.
        query: a mapping for field/query, e.g. first_name="aa"

    Returns:
        tuple: the new cursor, total results count, and a list of objects as a result
    """
    if not query:
        raise ValueError("at least one query parameter is required, e.g. age=10")

    new_cursor, count, result = self.store.find(cursor_=cursor_, limit_=limit_, **query)
    return (new_cursor, count, (self._get_object_from_config(data[KEY_FIELD_NAME], data) for data in result))
def get_instance_property(self, name)

return a new property descriptor for a given name, this will help in lazy-loading as this property will only create and load an instance from the store once accessed.

Args

name : str
instance name

Returns

property
property descriptor (object)
Expand source code
def get_instance_property(self, name):
    """
    return a new property descriptor for a given name, this will help in lazy-loading
    as this property will only create and load an instance from the store once accessed.

    Args:
        name (str): instance name

    Returns:
        property: property descriptor (object)
    """
    inner_name = f"__{name}"

    def getter(factory):
        # we need to avoid AttributeError which hasattr swallows
        # instead we check the internal __dict__
        if inner_name in factory.__dict__:
            return getattr(factory, inner_name)

        instance = self._get_object_from_config(name, factory.store.get(name))
        setattr(factory, inner_name, instance)
        return instance

    return property(getter)
def handle(self, ev)

handle when the parent instance is deleted

Args

ev : InstanceDeleteEvent
instance delete event
Expand source code
def handle(self, ev):
    """
    handle when the parent instance is deleted

    Args:
        ev (InstanceDeleteEvent): instance delete event
    """
    # do nothing if this is a root factory
    if not self.parent_instance or not self.parent_factory:
        return

    # handle deletion of children
    if isinstance(ev, InstanceDeleteEvent):
        if ev.factory == self.parent_factory and ev.name == self.parent_instance.instance_name:
            for name in self.list_all():
                self.delete(name)
def list_all(self)

get all instance names (stored or not)

Returns

set
instance names
Expand source code
def list_all(self):
    """
    get all instance names (stored or not)

    Returns:
        set: instance names
    """
    names = set(self.store.list_all())
    return names.union(super().list_all())
def new(self, name, *args, **kwargs)

get a new instance and make it available as an attribute

this method also initialize sub-factories for this instance.

Args

name : str
name
*args
arbitrary arguments passed to the factory Base type
*kwargs
arbitrary keyword arguments passed to the factory Base type

Raises

DuplicateError
in case an instance with the same name exists

Returns

Base
instance
Expand source code
def new(self, name, *args, **kwargs):
    """
    get a new instance and make it available as an attribute

    this method also initialize sub-factories for this instance.

    Args:
        name (str): name
        *args: arbitrary arguments passed to the factory `Base` type
        *kwargs: arbitrary keyword arguments passed to the factory `Base` type

    Raises:
        DuplicateError: in case an instance with the same name exists

    Returns:
        Base: instance
    """
    instance = super().new(name, *args, **kwargs)
    self._init_save_and_sub_factories(instance)
    return instance

Inherited members