Coder Social home page Coder Social logo

lancetnik / fastdepends Goto Github PK

View Code? Open in Web Editor NEW
209.0 209.0 6.0 557 KB

FastDepends - FastAPI Dependency Injection system extracted from FastAPI and cleared of all HTTP logic. Async and sync modes are both supported.

Home Page: https://lancetnik.github.io/FastDepends/

License: MIT License

Python 99.53% Shell 0.47%
async dependency dependency-injection fastapi faststream propan python sync

fastdepends's Introduction

Hi there ๐Ÿ‘‹ I'm @Lancetnik (Pastukhov Nikita)

I'm a fullstack developer living and working in Russia. ๐Ÿ‡ท๐Ÿ‡บ

I have been building APIs and tools for Machine Learning and data systems, with different teams and organizations. ๐ŸŒŽ

I created Propan built FastStream๐Ÿš€ and currently spend a huge part of my time to work on it. ๐Ÿค“

So, if my open source projects are useful for your product/company, please tweet with @diementros or product mention about - your feedback is very important for me.

fastdepends's People

Contributors

dependabot[bot] avatar jonathanserafini avatar lancetnik avatar pythrick avatar v-sopov avatar vitailog avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

fastdepends's Issues

Replacement for python-dependency-injector

Hi,

We are using https://github.com/ets-labs/python-dependency-injector with our fastapi api, consisting of a repository/service pattern. Since with that library the async ability of fastapi is not possible and the fact that the library is not maintained anymore, we are looking for an alternatieve.

Would implementing a repisitory/service pattern with FastDepends be possible, and allow for async non blocking Datbase calls?

Thanks. Btw FastStreams looking really nice too!

ForwardRef annotations and non-global context breaks models resolution

If both of the following conditions are met, the PydanticUserError exception is raised during function call:

  1. ForwardRef annotations for arguments (e.g. with from __future__ import annotations or, even, just with string annotations)
  2. Usage of non-global context. For example in attempt to dynamically create inject in wrapped function.

My particular case was from usage of faststream, where I needed dynamically attach broker to different handlers. It all works well, but I have to be careful not to switch forward refs by accident.

Reproduction:

from __future__ import annotations  # all is working if you comment this line

import asyncio
from pydantic import BaseModel

from fast_depends import inject


def container_function():
    class Model(BaseModel):
        num: int

    @inject
    async def main(a: Model):
        pass

    return main


main = container_function()
asyncio.run(main({"num": 1}))

PS. Nice libraries, keep up great work!

Validation of non-pydantic types?

Hey there - love this package! Quick question - what's the best practice for having dependencies that aren't valid Pydantic types - for example, I'm using it to inject an API client like so:

@inject
async def cancel_running_flow_runs(
    flow_name: str,
    client: PrefectClient = Depends(prefect.get_client)
):

I get the error check that <class 'prefect.client.orchestration.PrefectClient'> is a valid pydantic field type

Now I could remove the type hint and it would work but I'd lose type hints within the function. I might be missing something obvious in the docs but curious if there's a way to either add a new type or allow arbitrary types in arguments?

CustomField error with class-based dependency

Seeing this error:

class TestProvider(CustomField):
    def __init__(self):
        print('init')
        super().__init__(cast=True)
        
    def use(self, **kwargs) -> typing.Dict[str, typing.Any]:
        kwargs = super().use(**kwargs)
        if self.param_name:
            kwargs[self.param_name] = 'test'
        return kwargs
    
class Tester:
    @inject
    def __init__(self, test: str = TestProvider()):
        print(test)

Tester()

Raises this error:

TypeError                                 Traceback (most recent call last)
Cell In[32], [line 17](vscode-notebook-cell:?execution_count=32&line=17)
     [13](vscode-notebook-cell:?execution_count=32&line=13)     @inject
     [14](vscode-notebook-cell:?execution_count=32&line=14)     def __init__(self, test: str = TestProvider()):
     [15](vscode-notebook-cell:?execution_count=32&line=15)         print(test)
---> [17](vscode-notebook-cell:?execution_count=32&line=17) Tester()

File [/lib/python3.11/site-packages/fast_depends/use.py:156](.../lib/python3.11/site-packages/fast_depends/use.py:156), in _wrap_inject.<locals>.func_wrapper.<locals>.injected_wrapper(*args, **kwargs)
    [153](.../lib/python3.11/site-packages/fast_depends/use.py:153) @wraps(func)
    [154](.../lib/python3.11/site-packages/fast_depends/use.py:154) def injected_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
    [155](.../lib/python3.11/site-packages/fast_depends/use.py:155)     with ExitStack() as stack:
--> [156](.../lib/python3.11/site-packages/fast_depends/use.py:156)         r = real_model.solve(
    [157](.../lib/python3.11/site-packages/fast_depends/use.py:157)             *args,
    [158](.../lib/python3.11/site-packages/fast_depends/use.py:158)             stack=stack,
    [159](.../lib/python3.11/site-packages/fast_depends/use.py:159)             dependency_overrides=overrides,
    [160](.../lib/python3.11/site-packages/fast_depends/use.py:160)             cache_dependencies={},
    [161](.../lib/python3.11/site-packages/fast_depends/use.py:161)             nested=False,
    [162](.../lib/python3.11/site-packages/fast_depends/use.py:162)             **kwargs,
    [163](.../lib/python3.11/site-packages/fast_depends/use.py:163)         )
    [164](.../lib/python3.11/site-packages/fast_depends/use.py:164)         return r
    [165](.../lib/python3.11/site-packages/fast_depends/use.py:165)     raise AssertionError("unreachable")

File [/lib/python3.11/site-packages/fast_depends/core/model.py:309](.../lib/python3.11/site-packages/fast_depends/core/model.py:309), in CallModel.solve(self__, stack, cache_dependencies, dependency_overrides, nested, *args, **kwargs)
    [300](.../lib/python3.11/site-packages/fast_depends/core/model.py:300)     kwargs[dep_arg] = dep.solve(
    [301](.../lib/python3.11/site-packages/fast_depends/core/model.py:301)         stack=stack,
    [302](.../lib/python3.11/site-packages/fast_depends/core/model.py:302)         cache_dependencies=cache_dependencies,
   (...)
    [305](.../lib/python3.11/site-packages/fast_depends/core/model.py:305)         **kwargs,
    [306](.../lib/python3.11/site-packages/fast_depends/core/model.py:306)     )
    [308](.../lib/python3.11/site-packages/fast_depends/core/model.py:308) for custom in self__.custom_fields.values():
--> [309](.../lib/python3.11/site-packages/fast_depends/core/model.py:309)     kwargs = custom.use(**kwargs)
    [311](.../lib/python3.11/site-packages/fast_depends/core/model.py:311) final_args, final_kwargs = cast_gen.send(kwargs)
    [313](.../lib/python3.11/site-packages/fast_depends/core/model.py:313) if self__.is_generator and nested:

TypeError: TestProvider.use() got multiple values for argument 'self'

Removal of _contrib.CreateBaseModel seems backwards incompatible

Due to another bug in FastStream airtai/faststream#1087 I am trying to use older versions of FastStream.

FastStream has FastDepends as a dep, pinned to >=2.x, <3 https://github.com/airtai/faststream/blob/ac6d0edd66ee61dca011a5b16198c3e91c485cff/pyproject.toml#L51

As the removal of _contrib.CreateBaseModel is done from 2.6.0 and 2.7.0, installs of older versions of faststream are now failing as they are defaulting to 2.7.0 but require the removed CreateBaseModel.

Weird dependency injection issue

Hey there! Me again ๐Ÿ˜ตโ€๐Ÿ’ซ (btw - big fan of this and the FastStream project!)

Running into this issue in the context of FastStream.

The following code works as expected:

import os
import typing
from redis import Redis, ConnectionPool
from pydantic import BaseModel, Field
from faststream import FastStream, Depends, ContextRepo, Context
from faststream.kafka import KafkaBroker
from faststream.kafka.annotations import (
    Logger,
    KafkaBroker as BrokerAnnotation,
)


KAFKA_HOST = os.environ.get('KAFKA_HOST', 'localhost')
KAFKA_PORT = os.environ.get('KAFKA_PORT', '19092')

broker = KafkaBroker(f'{KAFKA_HOST}:{KAFKA_PORT}')
app = FastStream(broker)


class SimpleDependency(BaseModel):
    id: int


def simple_dependency():
    return SimpleDependency(id=1)


class SimpleMessage(BaseModel):
    message: str


class ComplexMessage(SimpleMessage):
    id: int = Field(..., gt=0)
    message: str = Field(..., min_length=1)


class NestedMessage(BaseModel):
    message: SimpleMessage
    id: int


def inject_redis_client() -> Redis:
    return Redis(
        connection_pool=ConnectionPool(
            host=os.environ.get('REDIS_HOST', 'localhost'),
            port=int(os.environ.get('REDIS_PORT', '6379')),
            db=int(os.environ.get('REDIS_DB', '0'))
        )
    )


@broker.subscriber('testing-a')
async def testing_a(
    message: ComplexMessage,
    logger: Logger,
    redis: Redis = Depends(inject_redis_client),
    dep: SimpleDependency = Depends(simple_dependency),
):
    await broker.publish(
        NestedMessage(
            message=message,
            id=dep.id
        ),
        'testing-b'
    )

@broker.subscriber('testing-b')
async def testing_b(
    message: NestedMessage,
    logger: Logger,
    dep: SimpleDependency = Depends(simple_dependency),
):
    logger.info(message)
    logger.info(dep)


@app.after_startup
async def after_startup(
    context: ContextRepo,
    logger: Logger,
):
    for i in range(0, 10):
        await broker.publish(ComplexMessage(id=i+1, message=f'hello {i}'), 'testing-a')

testing_a correctly receives message: ComplexMessage and the redis client instance is available.

However, if change my inject_redis_client function to accept an argument, for instance:

def inject_redis_client(
    redis_db: typing.Optional[int] = None
) -> Redis:
    return Redis(
        connection_pool=ConnectionPool(
            host=os.environ.get('REDIS_HOST', 'localhost'),
            port=int(os.environ.get('REDIS_PORT', '6379')),
            db=redis_db or int(os.environ.get('REDIS_DB', '0'))
        )
    )

Then when I run the app I get the following error:

2023-09-22 10:28:19,730 INFO     - testing-a | 449-169540 - Received
2023-09-22 10:28:19,731 ERROR    - testing-a | 449-169540 - ValidationError: 1 validation error for testing_a
message
  Input should be a valid dictionary or instance of ComplexMessage [type=model_type, input_value='hello 9', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/model_type
Traceback (most recent call last):
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/faststream/broker/core/asyncronous.py", line 550, in log_wrapper
    r = await func(message)
        ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/faststream/kafka/broker.py", line 240, in process_wrapper
    r = await self._execute_handler(func, message)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/faststream/broker/core/asyncronous.py", line 487, in _execute_handler
    return await func(message)
           ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/faststream/broker/core/asyncronous.py", line 415, in decode_wrapper
    return await func(**msg)
           ^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/fast_depends/use.py", line 135, in injected_wrapper
    r = await real_model.asolve(
        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/fast_depends/core/model.py", line 396, in asolve
    final_args, final_kwargs = cast_gen.send(kwargs)
                               ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/fast_depends/core/model.py", line 211, in _solve
    casted_model = self.model(**solved_kw)
                   ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/example/lib/python3.11/site-packages/pydantic/main.py", line 165, in __init__
    __pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)
pydantic_core._pydantic_core.ValidationError: 1 validation error for testing_a
message
  Input should be a valid dictionary or instance of ComplexMessage [type=model_type, input_value='hello 9', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/model_type
^C2023-09-22 10:28:21,580 INFO     - FastStream app shutting down...
2023-09-22 10:28:21,581 INFO     - FastStream app shut down gracefully.

In this context, I'm not passing a redis_db argument to the testing_a function - but that shouldn't matter as it's optional.

Even if I update the function to set a value:

@broker.subscriber('testing-a')
async def testing_a(
    message: ComplexMessage,
    logger: Logger,
    redis_db: int = 1,
    redis: Redis = Depends(inject_redis_client),
    dep: SimpleDependency = Depends(simple_dependency),
):
    await broker.publish(
        NestedMessage(
            message=message,
            id=dep.id
        ),
        'testing-b'
    )

I get the same issue.

I might not 100% be understanding how you're supposed to pass arguments to dependency injection functions but nothing I try seems to make a difference.

Incomplete Execution of Yielded Dependencies Upon Abnormal Termination

Python version: 3.12.2

When a dependency is yielded within a function decorated with @inject, it is expected that the entire dependency coroutine would execute to completion before any subsequent code is executed. However, in scenarios where the application is terminated abruptly, the coroutine seems to be interrupted before it completes its execution.

To illustrate the issue, I've provided a minimal example below:

import asyncio
from typing import AsyncIterator

from fast_depends import Depends, inject

async def sample_dep() -> AsyncIterator[int]:
    print("before")
    yield 10
    print("after")

@inject
async def a_random_function(numb: int = Depends(sample_dep)) -> None:
    print(numb + 10)
    await asyncio.sleep(2)

async def main() -> None:
    try:
        await a_random_function()
    except asyncio.CancelledError:
        print("cancelled")

if __name__ == "__main__":
    asyncio.run(main())

In the provided example, the text "after" is expected to be printed after the value 10 is yielded from the sample_dep coroutine. However, if the application is terminated abruptly, such as by a keyboard interrupt, the "after" text is not printed, indicating that the coroutine was not allowed to complete its execution.

I believe this behavior may lead to unexpected results and could potentially cause issues in real-world applications where the completion of dependencies is crucial (like database or broker connections)

Could you please investigate this behavior and provide guidance on how to ensure that yielded dependencies are executed to completion even in scenarios of abnormal termination?

Appreciate your assistance with this matter and your contributions to this awesome library.
Thank you.

Use in class

Hi there!

Quick question - is there a way to use this in the context of a class rather than a function?

Either within a method:

class Example:
    @inject
    def method(self, x = Depends(dependency)):
        ...

or class variable?

@inject
class Example:
    x = Depends(dependency)
    
    def method(self):
        ...

Right now, in the first example, I get the error TypeError: CallModel.solve() got multiple values for argument 'self'

(second example was just another idea - not expected behavior).

Love the library btw - thanks!

Using @inject on generator function raises `RuntimeError: generator didn't stop`

In this mock example:

from fast_depends import Depends, inject

class Dependency:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"Dependency({self.name})"


def get_dependency():
    return Dependency("dependency")


@inject
def generator(
    d: Dependency = Depends(get_dependency),
):
    for i in range(10):
        yield i

When I then try to call and iterate on generator(), I get the following error:

----> [1](vscode-notebook-cell:/Users/chrisgoddard/Code/EdgePath/edgepath-pilot-core/notebooks/syntax.ipynb#W4sZmlsZQ%3D%3D?line=0) for x in generator():
      [2](vscode-notebook-cell:/Users/chrisgoddard/Code/EdgePath/edgepath-pilot-core/notebooks/syntax.ipynb#W4sZmlsZQ%3D%3D?line=1)     print(x)

File [/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/site-packages/fast_depends/use.py:128](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/site-packages/fast_depends/use.py:128), in _wrap_inject.<locals>.func_wrapper.<locals>.injected_wrapper(*args, **kwargs)
    126 @wraps(func)
    127 def injected_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
--> 128     with ExitStack() as stack:
    129         r = real_model.solve(
    130             *args,
    131             stack=stack,
   (...)
    134             **kwargs,
    135         )
    136         return r

File [/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:589](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:589), in ExitStack.__exit__(self, *exc_details)
    585 try:
    586     # bare "raise exc_details[1]" replaces our carefully
    587     # set-up context
    588     fixed_ctx = exc_details[1].__context__
--> 589     raise exc_details[1]
    590 except BaseException:
    591     exc_details[1].__context__ = fixed_ctx

File [/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:574](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:574), in ExitStack.__exit__(self, *exc_details)
    572 assert is_sync
    573 try:
--> 574     if cb(*exc_details):
    575         suppressed_exc = True
    576         pending_raise = False

File [/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:148](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/Caskroom/miniforge/base/envs/edgepath-pilot-core/lib/python3.11/contextlib.py:148), in _GeneratorContextManager.__exit__(self, typ, value, traceback)
    146         return False
    147     else:
--> 148         raise RuntimeError("generator didn't stop")
    149 else:
    150     if value is None:
    151         # Need to force instantiation so we can reliably
    152         # tell if we get the same exception back

RuntimeError: generator didn't stop

I can see that there is logic in the CallModel class for is_generator but I'm not sure if there's something in how I write the generator or write the type hints that is the issue.

`Iterator[...]` generator return type support

Correct python annotation is broker in a FastDepends case:

from typing import Iterator
from fast_stream import inject

@inject
def func() -> Iterator[int]:
    for i in range(10):
        return i

Interop with FastAPI

Thanks for this awesome project!

I know the main goal is actually to provide a stand-alone alternative but I'm curious if there's a way (or plans) to support interoperability with the FastAPI dependency system.

The main use case I can think of is the following:

  • In a larger code base, I want to be able to create some libraries that can be re-used in different contexts. E.g. a db package with connection pool setup dependencies. Sometimes that might be included in a fastapi app, other times a fast-stream app, other times a cli tool. I would like to be able to share the dependency configuration.
  • This is similar in spirit to how the guice or dagger dependency frameworks on the jvm allow you to create DI modules that can be composed and included (e.g. in a Spring boot or Jetty server).

Depends type annotation

Hi there!

Currently, the Depends type annotation has it returning an instance of model.Depends

def Depends(
    dependency: Callable[P, T],
    *,
    use_cache: bool = True,
    cast: bool = True,
) -> model.Depends:
    return model.Depends(
        dependency=dependency,
        use_cache=use_cache,
        cast=cast,
    )

I wonder, wouldn't it make more sense if it returned T so that it passes the type checker when used in this way:

def some_function(
    dependency: SomeClass = Depends(get_dependency)
):

When get_dependency returns an object of type SomeClass.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.