reagento / dishka Goto Github PK
View Code? Open in Web Editor NEWCute DI framework with agreeable API and everything you need
Home Page: https://dishka.readthedocs.io
License: Apache License 2.0
Cute DI framework with agreeable API and everything you need
Home Page: https://dishka.readthedocs.io
License: Apache License 2.0
Allow to set alias when entering scope. That will allow to change logic for specific route without moving it to container
with container(aliases={A: A1}) as request_container:
request_container.get(A) # this will reuturn A1
Aliases should be copied to inner scopes if we do not know the scope of depedency. But the do not affect the outer scope.
special marker to provide element of a list or mapping
Create something depending on, at least, request data
Do you happen to have a fastapi sqlalchemy async example to use this library with?
We need to ensure that all required dependencies can be actually created on startup.
Do not allow to override dependency provider unless it is explicitly declared
Use case:
Interactor
depends on StatisticDbGateway
and MainDbGateway
. Each of them uses SQLAlchemy Session
object, so they have similar init typehints. The difference is that StatisticDbGateway
requires Session
bound to Clickhouse Engine
, while MainDbGateway
binds to postgresql Engine
. Again both engines have same type, but created differently.
To solve this you can replace Session
with some new type (literally NewType("PgSession", Session)
), but it requires modification of all code base. Imagine that you took those objects from external library and cannot modify it.
Another solution is again to use NewType
, but create factories for all objects instead of registrering classes as depedencies directly. This requries a lot of work.
Proposed solution is to split those objects in 3 groups:
SubGraph1: StatisticDbGateway
, Session
, Engine
SubGraph2: MainDbGateway
, Session
, Engine
MainGraph: Interactor
, SubGraph1
, SubGraph2
Each graph here uses only his factories to solve dependencies or requests specially attached subgraphs. So here, gateways can request Session and they get exactly that session which is declared in his subgraph.
Requirements:
It is an open question whether (sub)graph is just a container or another thing
class A: ...
class B(A): ...
p = Provider(scope=Scope.APP)
p.provide(B)
c = make_container(p)
c.get(A) # expected B()
Let's image we have 3 classes:
class C(Generic[T]):
pass
class Gw(Generic[T]):
def __init__(self, c: C[T]): ...
class U:
def __init__(self, Gw[Model]): ...
We should not declare factories for concrete intermediate classes like Gw in example. E.g. this is expected to work:
class MyProvider(Provider):
@provide(scope=Scope.APP)
def c_model(self) -> C[Model]:
return CModelImpl()
gw = provide(Gw, scope=Scope.APP)
u = provide(U, scope=Scope.APP)
...
container.get(U)
I'm moving from the python-dependency-injector
library to this one and am using a service-repo pattern.
This is my small setup:
class DBProvider(Provider):
@provide(scope=Scope.APP)
def engine(self) -> AsyncEngine:
engine = create_async_engine(
get_config().ASYNC_DB_URL(),
echo=True,
)
return engine
@provide(scope=Scope.REQUEST)
async def session(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
async with AsyncSession(engine) as session:
yield session
class Interactor:
def __init__(self, session: AsyncSession):
self.session = session
def test(self) -> str:
return "test"
class InteractorProvider(Provider):
i1 = provide(Interactor, scope=Scope.REQUEST)
class ServiceProvider(Provider):
wallet_pass_api_service = provide(WalletPassApiService, scope=Scope.APP)
block_user_repo = provide(BlockedUserRepository, scope=Scope.REQUEST)
container = make_async_container(
DBProvider(),
ServiceProvider(),
InteractorProvider()
)
When I use Scope.APP
with the block_user_repo
this does not work, as expected, since the BlockedUserRepositoryhas a dependency on the
AsyncSessionwhich is a
Scope.REQUEST`.
But shouldn't all repositories and service be initialised on app startup? Because creating, possibly 10 or 20 repositories/services on each request result in large overhead?
It is inconvenient to use additional strings/constants for the sake of components.
class TelegramProvider(Provider):
component = "Telegram"
...
@provide(scope=Scope.REQUEST)
async def get_user(self, telegram_object: TelegramObject) -> User:
return telegram_object.from_user
@router.message()
async def handler(
message: Message,
user: Annotated[User, FromComponent("Telegram")],
) -> None: ...
It would be convenient to add MyProvider
to the annotation, and then use its component
field under the hood of the library.
@router.message()
async def handler(
message: Message,
user: Annotated[User, TelegramProvider],
) -> None: ...
This solution has one more advantage: we get the opportunity to move to the provider in 1 click (e.g. to edit it).
This is not possible in the current implementation (but it's possible in fastapi.Depends)
This solution also helps to avoid pyCharm bug with brackets highlighting: https://youtrack.jetbrains.com/issue/PY-57155/
How can I use injection on classes which are initialised on each route? The following code results in this error which is logical because I use FastAPI Depends
. The error:
raise fastapi.exceptions.FastAPIError(
fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'common.services.permission_service.PermissionService'> is a valid Pydantic field type. If you are using a return type annotation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None. Read more: https://fastapi.tiangolo.com/tutorial/response-model/
My auth route:
@router.post('/token', response_model=BaseResponse[GetAuthTokenResponse])
@inject
async def token_authorize(request_data: AuthTokenRequest, user: AnnotatedAuthenticatedUser, auth_service: Annotated[AuthService, FromDishka()]):
user, token = ....
return schemas.BaseResponse[GetAuthTokenResponse](data={
'user': user,
'token': token.decode(),
})
the AnnotatedAuthenticatedUser
:
AnnotatedAuthenticatedUser = Annotated[AuthenticatedUser, Depends(AuthCheck())]
And the AuthCheck
:
class AuthCheck:
def __init__(self, permission: AuthCheckPermission = None):
self._permission = permission
def __call__(self,
res: Response,
req: Request,
permission_service: Annotated[PermissionService, FromDishka()],
invite_service: Annotated[InviteService, FromDishka()],
event_service: Annotated[event_service.EventService, FromDishka()],
organisation_service: Annotated[OrganizationService, FromDishka()],
cred: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False)),
):
# with trace.get_tracer(__name__).start_as_current_span("auth_check") as _:
if cred is None:
raise InvalidAuthException
try:
decoded_token = auth.verify_id_token(cred.credentials)
except Exception as err:
raise InvalidAuthCredentialsException(original_exception=err)
...
Add function to render all dependencies into one file. HTML with Mermaid can be good for start
This code works correctly, but when I tried to support multiple interfaces for AsyncSession I got an exception
@provide(scope=Scope.REQUEST)
async def get_session(self, session_maker: async_sessionmaker[AsyncSession]) -> AsyncIterable[AsyncSession],
async with session_maker() as session:
yield session
This code already doesn't work
@provide(scope=Scope.REQUEST)
async def get_session(self, session_maker: async_sessionmaker[AsyncSession]) -> AnyOf[
AsyncIterable[AsyncSession],
AsyncIterable[interfaces.UoW]
]:
async with session_maker() as session:
yield session
Exception:
dependency_source/make_factory.py", line 269, in _make_factory_by_method
raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
TypeError: Failed to analyze `Container.get_session`.
Unsupported return type `<dishka.entities.provides_marker.ProvideMultiple object at 0x7faea03bdb10>` for async generator. Did you mean AsyncIterable[<dishka.entities.provides_marker.ProvideMultiple object at 0x7faea03bdb10>]?
Sometimes we want to have additional itnermediate scopes in some run configurations. E.g
RUNTIME
scope before APP
REQUEST
scope.The idea consists of two parts:
skipped
when entering wihout explicit scope.So, if you enter context you
a) either provide scope explicitely, then all intermediate scopes will be also entered
b) do not provide any scope, then it is treated as the next non-skippable scopes. All scopes with mark skip
will be enterd automatically
E.g.
with container(request=value) as subcontainer:
...
request here should be available in solving context.
Or using typehint due to we don't have named dependencies:
with container({Request: value}) as subcontainer:
...
from abc import ABC
from dishka import make_container, Provider, Scope
from typing import Generic, TypeVar
class Event(ABC):
...
EventsT = TypeVar("EventsT")
class EventEmitter(Generic[EventsT], ABC):
...
class EventEmitterImpl(EventEmitter[EventsT]):
...
def factory(event_emitter: EventEmitter[Event]) -> int:
return 1
provider = Provider()
provider.provide(EventEmitterImpl, scope=Scope.REQUEST, provides=EventEmitter)
provider.provide(factory, scope=Scope.REQUEST)
container = make_container(provider)
Traceback (most recent call last):
File "/home/lubaskincode/.config/JetBrains/PyCharm2023.3/scratches/test.py", line 32, in <module>
container = make_container(provider)
File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/container.py", line 172, in make_container
).build()
File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/registry.py", line 340, in build
GraphValidator(registries).validate()
File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/registry.py", line 161, in validate
for factory in registry.factories.values():
RuntimeError: dictionary changed size during iteration
Clone container with adding/replacement of providers
As discussed on Podlodka Presentation
, traceback have multiple Dishka
internal calls between user incorrect code and traceback of exception, for example, dependency loop found
. Quite useful to grab a metadata of user code, such as filename and position, and place that information close to ASCII image of dependency loop.
As discussed earlier, it would be great to publish a comparative analysis of the performance of Dishka against Depends from FastAPI, as well as against pure use, where the required dependency is obtained directly (without additional tools)
I believe that this will make it easier to decide whether to use this library or not
Allow using dishka container to provide fixtures
AsyncSessionMaker = async_sessionmaker
class DbProvider(Provider):
def __init__(self, connection_string: str | AnyUrl, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.connection_string = str(connection_string)
@provide(scope=Scope.APP)
async def get_engine(self) -> AsyncEngine:
dsn = self.connection_string.replace("postgresql", "postgresql+asyncpg")
return create_async_engine(dsn)
@provide(scope=Scope.APP)
async def get_sessionmaker(self, engine: AsyncEngine) -> AsyncSessionMaker:
return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@provide(scope=Scope.REQUEST)
async def get_db_session(self, sessionmaker: AsyncSessionMaker) -> AsyncSession:
return await get_or_create_db_session(sessionmaker)
dishka.exceptions.NoFactoryError: Cannot find factory for
DependencyKey(type_hint=<class 'sqlalchemy.ext.asyncio.session.async_sessionmaker'>, component='')
requested by DependencyKey(type_hint=<class 'sqlalchemy.ext.asyncio.session.AsyncSession'>, component='').
It is missing or has invalid scope.```
If my object supports few interfaces I need to write wrapper for each one.
Can you support some syntaxes for single object which support several interfaces?
For example:
@provide(scope=Scope.REQUEST)
def get_db(self) -> I1 | I2 | I3:
return object()
dishka/src/dishka/error_rendering.py
Line 46 in 2ba2f1c
ะะฐะปััะต ะฒะธะดะฝะพ, ััะพ ะฝะต-optional ะฐัะณัะผะตะฝั ะฟัะพะฒะตััะตััั ะฝะฐ None.
Multiple Asynchronous sqlalchemy calls are not working properly with the following code, synchronous does.
I read that I need to have an AsyncSession
per asyncio task. How could I instruct Dishka to handle this?
class DBProvider(Provider):
@provide(scope=Scope.APP)
def engine(self) -> AsyncEngine:
engine = create_async_engine(
get_config().ASYNC_DB_URL(),
echo=get_config().QUERY_ECHO,
echo_pool=get_config().ECHO_POOL,
pool_pre_ping=True,
pool_size=get_config().DB_POOL_SIZE,
json_serializer=custom_json_serialiser
)
return engine
@provide(scope=Scope.REQUEST)
async def session(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
async with AsyncSession(engine, expire_on_commit=False) as session:
yield session
Feed service:
class FeedService:
def __init__(self, feed_item_repository: FeedItemRepository) -> None:
self._feed_item_repository: FeedItemRepository = feed_item_repository
async def get_organisation_feed(self, organisation_id: str, page: int = None, size: int = 20) -> List[FeedItem]:
return await self._feed_item_repository.get_for_organisation(organisation_id, page, size, load_all=True)
async def get_organisations_feed(self, organisation_ids: List[str], page: int = None, size: int = 20) -> List[FeedItem]:
feed_items = []
async def get_organisation_feed_task(organisation_id):
organisation_feed = await self.get_organisation_feed(organisation_id)
for feed_item in organisation_feed:
feed_items.append(feed_item)
tasks = []
for organisation_id in organisation_ids:
tasks.append(asyncio.create_task(get_organisation_feed_task(organisation_id)))
await asyncio.gather(*tasks)
return feed_items
Feed Item repo:
class FeedItemRepository(BaseRepository[FeedItem]):
_model = FeedItem
async def get_for_organisation(self, organisation_id: str, page: int = None, size: int = None, load_all: bool = False) -> list[FeedItem]:
q = select(self._model).filter(self._model.organisation_id == organisation_id)
if load_all:
q = q.options(
# joinedload(self._model.user),
joinedload(self._model.organisation),
joinedload(self._model.organisation),
joinedload(self._model.event),
joinedload(self._model.group),
joinedload(self._model.message),
joinedload(self._model.message_parent),
)
if page is not None and size is not None:
q = q.limit(size).offset(page * size)
q = q.order_by(self._model.created_at.desc())
return (await self.session.execute(q)).scalars().unique().all()
When I call the FeedService.get_organisations_feed
method it complains about this following:
Traceback (most recent call last):
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/starlette.py", line 41, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 758, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 778, in app
await route.handle(scope, receive, send)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 299, in handle
await self.app(scope, receive, send)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 79, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 74, in app
response = await func(request)
^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/routing.py", line 278, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/base.py", line 161, in autoinjected_func
return await func(*args, **kwargs, **solved)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/api/routes/me.py", line 475, in get_me_feed
feed_items = await feed_service.get_organisations_feed(user.organisation_ids_permissions, page=page)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 32, in get_organisations_feed
await asyncio.gather(*tasks)
File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 24, in get_organisation_feed_task
organisation_feed = await self.get_organisation_feed(organisation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 17, in get_organisation_feed
return await self._feed_item_repository.get_for_organisation(organisation_id, page, size, load_all=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/common/repositories/feed_item_repo.py", line 33, in get_for_organisation
return (await self.session.execute(q)).scalars().unique().all()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 452, in execute
result = await greenlet_spawn(
^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 186, in greenlet_spawn
result = context.switch(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2306, in execute
return self._execute_internal(
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2181, in _execute_internal
conn = self._connection_for_bind(bind)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2050, in _connection_for_bind
return trans._connection_for_bind(engine, execution_options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 2, in _connection_for_bind
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py", line 103, in _go
self._raise_for_prerequisite_state(fn.__name__, current_state)
File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 945, in _raise_for_prerequisite_state
raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: This session is provisioning a new connection; concurrent operations are not permitted (Background on this error at: https://sqlalche.me/e/20/isce)
During handling of the above exception, another exception occurred:
+ Exception Group Traceback (most recent call last):
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
| result = await app( # type: ignore[func-returns-value]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
| return await self.app(scope, receive, send)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__
| await super().__call__(scope, receive, send)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/applications.py", line 123, in __call__
| await self.middleware_stack(scope, receive, send)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__
| raise exc
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
| await self.app(scope, receive, _send)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/cors.py", line 83, in __call__
| await self.app(scope, receive, send)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py", line 24, in __call__
| await responder(scope, receive, send)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py", line 44, in __call__
| await self.app(scope, receive, self.send_with_gzip)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/starlette.py", line 37, in __call__
| async with request.app.state.dishka_container(
| dishka.exceptions.ExitError: Cleanup context errors (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/async_container.py", line 157, in close
| await anext(exit_generator.callable)
| File "/Users/theobouwman/dev/projects/momo/momo-api/di.py", line 79, in session
| async with AsyncSession(engine, expire_on_commit=False) as session:
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 1071, in __aexit__
| await asyncio.shield(task)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 1016, in close
| await greenlet_spawn(self.sync_session.close)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 186, in greenlet_spawn
| result = context.switch(*args, **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2462, in close
| self._close_impl(invalidate=False)
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2531, in _close_impl
| transaction.close(invalidate)
| File "<string>", line 2, in close
| File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py", line 121, in _go
| raise sa_exc.IllegalStateChangeError(
| sqlalchemy.exc.IllegalStateChangeError: Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5> (Background on this error at: https://sqlalche.me/e/20/isce)
MRO
from random import random
from dishka import provide, Provider, Scope
class MyProvider(Provider):
scope = Scope.APP
rand = provide(source=random, provides=float)
Add LazyProxy and put it instead of requested class when cycle is detected.
It should be enabled explicitly for each cycle
Add option to use lock in container/subcontainer
if I've forgot scope i received this error
E KeyError: None
..\..\AppData\Local\pypoetry\Cache\virtualenvs\shvatka-7O9jFSMq-py3.11\Lib\site-packages\dishka\registry.py:84: KeyError
please add to error provider name (and will be greate if method name or provided type for case with really many @provide
functiuons in one provider)
if I created sync container and add async provider i receive next error:
ValueError: Unsupported type FactoryType.ASYNC_GENERATOR
will be greate to add to error why (wrong type of container)
Sometimes (especially when we follow interface segregation principle) we have single implementation for multiple dependencies. It would be useful to have single instance for all of them.
Let's
Current way to make alias:
class MyProvider(Provider):
@provide(scope=MyScope.REQUEST)
def get_repo(self, repo: UserRepositoryImpl) -> UserGetter:
return repo
We can rely on the fact that scope must be the same, so this can be simplified to:
class MyProvider(Provider):
repo = alias(UserRepositoryImpl, dependency=UserGetter)
it is exhausting to declare scope for each depedency as most of the will have Request scope. It woul be easier to set it on Provder level. E.g
class MyProvider(Provider):
scope = Scope.REQUEST
a = provide(A) # will have Scope.REQUEST
b = provide(B, scope=Scope.APP) # will have Scope.APP
Sometimes we have a dependency provided by library and we want to modify it (like decorating).
So, the next provider will receive some dependency (+additional), and provide the same type of dependency
container.invalidate(Pool)
to remove an object from cache so it can be recreated
When we finalize dependencies we have no information if there were exception during process handling.
We can use send
or asend
to pass it without breaking compatibility.
Also we will need to add an optional parameter to close
method if Container
object and modify it's __exit__
Add tests and ensure that everything is working as intended.
@provide(provides=A)
@provide(provides=AProto)
def foo(self, a: A) -> A:
return A()
@decorate(provides=A)
@decorate(provides=AProto)
def bar(self, a: A) -> A:
return A()
E.g. fastapi app and bot in one process will have single app container
Idea:
I suggest adding the ability to disable lazy dependency loading for the 'provide' function or globally for the entire 'Provider'
How is it supposed to work?
When creating a new container, create all dependencies with the 'lazy=False' flag and add them to the cache.
Why do you need it?
Create resource-intensive sessions or pools, as well as detect errors early when creating dependencies and protect against errors during code execution
How I see it in the code
Provide
class MyProvider(Provider)
a = provide(A, scope=Scope.App, lazy=False)
Provider
class MyProvider(Provider)
lazy = Fasle
a = provide(A, scope=Scope.App)
Hey!
Is it possible override dependecies like it realized in dependency-injector?
with container.api_client_factory.override(unittest.mock.Mock(ApiClient)):
service2 = container.service_factory()
assert isinstance(service2.api_client, unittest.mock.Mock)
Hi,
How could one use dishka with FastStream? https://github.com/airtai/faststream
Hi! I created DbProvider
:
AsyncSessionMaker = async_sessionmaker[AsyncSession]
_db_session_ctx: ContextVar[AsyncSession | None] = ContextVar("_db_session_ctx", default=None)
class ContextSessionMaker(AsyncSessionMaker):
def __call__(self, **local_kw) -> AsyncSession:
session = _db_session_ctx.get()
if session is None:
session = super().__call__(**local_kw)
_db_session_ctx.set(session)
return session
class DbProvider(Provider):
def __init__(self, conn_string: str, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.conn_string = conn_string
@provide(scope=Scope.APP)
async def get_engine(self) -> AsyncEngine:
return create_async_engine(self.conn_string)
@provide(scope=Scope.APP)
async def get_sessionmaker(self, engine: AsyncEngine) -> ContextSessionMaker:
return ContextSessionMaker(engine, class_=AsyncSession, expire_on_commit=False)
@provide(scope=Scope.REQUEST)
async def get_db_session(
self, sessionmaker: ContextSessionMaker
) -> AsyncIterable[AsyncSession]:
async with sessionmaker() as db_session:
yield db_session
But in tests i got error:
dishka.exceptions.NoFactoryError: Cannot find factory for (<class 'sqlalchemy.ext.asyncio.session.AsyncSession'>, component=''). Check scopes in your providers. It is missing or has invalid scope.
@pytest.fixture()
async def container() -> AsyncIterator[dishka.AsyncContainer]:
db_provider = DbProvider("sqlite+aiosqlite:///:memory:")
container = dishka.make_async_container(db_provider)
yield container
await container.close()
@pytest.fixture()
async def db_session(container: dishka.AsyncContainer) -> AsyncSession:
return await container.get(AsyncSession)
dishka version: 1.1.1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.