ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.83k stars 304 forks source link

ListProvider with config from yaml #443

Open milokmet opened 3 years ago

milokmet commented 3 years ago

Hello Roman, I am playing a little bit with dependency_injector and I don't know, how could I configure in yaml and write into Container ListProvider next example:

Is it possible to rewrite this with dependency_injector?

from abc import ABC, abstractmethod
class Source(ABC):
    @abstractmethod
    def do_something(self):
        pass

class MultiSource(Source):
    _sources = []
    def __init__(self, sources=[]):
        self._sources = sources
    def add(self, source):
        if source not in self._sources:
            self._sources.append(source)
    def do_something(self):
        print('Starting to do something')
        for s in self._sources:
            s.do_something()

class OracleSource(Source):
    def __init__(self, dbparams):
        self.name = dbparams['dsn']
        # self._conn = cx_Oracle.connect(**dbparams)
        ...
    def do_something(self):
        # fetch from db and return
        print('fetching and returning from {}'.format(self.name))
        ...

class ListSource(Source):
    _values = []
    def __init__(self, values):
        self._values = values

    def do_something(self):
        print('fetching and returning from provided list')

src = MultiSource()
src.add(OracleSource({'dsn': 'db1'}))
src.add(OracleSource({'dsn': 'db2'}))
src.add(ListSource(['task1', 'task2']))

src.do_something()

And the yaml confg should like like this:

source:
  - type: OracleSource
    params:
      dsn: "db1"

  - type: ListSource
    tasks:
      - { name: task1 }
      - { name: task2 }

Will it be possible to detect by type of config? If config.source will be dict, it will configure the source by type directly, if it will be list, it will use MultiSource?

source:
  type: list
  tasks:
   - { name: task1 }
   - { name: task2 }

Is it possible to rewrite this into container with list provider a config?

Thanks

rmk135 commented 3 years ago

Hi @milokmet ,

Good question. No, there is nothing for now you could use to make it work out of the box. You need to have a factory method. Here is a closest implementation:

from abc import ABC, abstractmethod

from dependency_injector import containers, providers

class Source(ABC):
    @abstractmethod
    def do_something(self):
        pass

class OracleSource(Source):
    def __init__(self, params):
        self._params = params

    def do_something(self):
        print('Oracle', self._params)

class ListSource(Source):
    def __init__(self, tasks):
        self._tasks = tasks

    def do_something(self):
        print('List', self._tasks)

class MultiSource(Source):
    _sources = []
    def __init__(self, sources=None):
        self._sources = sources or []

    def add(self, source):
        if source not in self._sources:
            self._sources.append(source)

    def do_something(self):
        print('Starting to do something')
        for s in self._sources:
            s.do_something()

    @classmethod
    def create(cls, config, sources_factory):
        sources = []
        for source_config in config.copy():
            source_type = source_config.pop('type')
            source = sources_factory(source_type, **source_config)
            sources.append(source)
        return cls(sources)

class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    sources_factory = providers.FactoryAggregate(
        oracle=providers.Factory(OracleSource),
        list=providers.Factory(ListSource),
    )

    multi_source = providers.Factory(
        MultiSource.create,
        config=config.source,
        sources_factory=sources_factory.provider,
    )

if __name__ == '__main__':
    container = Container()
    container.config.from_yaml('workaround.yml')

    multi_source = container.multi_source()
    multi_source.do_something()

There is a new feature in the development to cover that type of cases: build container from schemas. That's how example schema looks like:

version: "1"

container:
  config:
    provider: Configuration

  database_client:
    provider: Singleton
    provides: sqlite3.connect
    args:
      - container.config.database.dsn

  s3_client:
    provider: Singleton
    provides: boto3.client
    kwargs:
      service_name: s3
      aws_access_key_id: container.config.aws.access_key_id
      aws_secret_access_key: container.config.aws.secret_access_key

  user_service:
    provider: Factory
    provides: schemasample.services.UserService
    kwargs:
      db: container.database_client

  auth_service:
    provider: Factory
    provides: schemasample.services.AuthService
    kwargs:
      db: container.database_client
      token_ttl: container.config.auth.token_ttl.as_int()

  photo_service:
    provider: Factory
    provides: schemasample.services.PhotoService
    kwargs:
      db: container.database_client
      s3: container.s3_client

Follow #337 if you're interested.

milokmet commented 3 years ago

Wow. I didn't expect an answer so quickly. Thank you very much. I am going to try it. The container builder from schema looks interesting.

rmk135 commented 3 years ago

Ok, cool. Let me know if you need any other help.