Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ jobs:
uses: codecov/codecov-action@v7
with:
env_vars: PYTHON
report-type: test_results
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests,python-${{ matrix.python-version }}

- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v7
with:
env_vars: PYTHON
report-type: coverage
report_type: coverage
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests,python-${{ matrix.python-version }}

Expand All @@ -69,4 +69,4 @@ jobs:
file: coverage.xml
language: Python
label: code-coverage/pytest
fail-on-error: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ venv
.venv
.pdm-build
build
typings

# vim temporary files
*~
Expand Down
12 changes: 5 additions & 7 deletions pyrannic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
__version__ = "0.5.6"
__version__ = "0.5.7"

from .application import Application as Application
from .bootstrap.service_provider import ServiceProvider as ServiceProvider
from .config.configuration import Configuration as Configuration
from .container.param_functions import Resolves as Resolves
from .ioc import Resolve as Resolve
from .database.migration import Migration as Migration
from .database.provider import DatabaseServiceProvider as DatabaseServiceProvider
from .http.exceptions.resource_not_found import (
ResourceNotFoundException as ResourceNotFoundException,
)
from .http.providers import (
ExceptionHandlersServiceProvider as ExceptionHandlersServiceProvider,
)
from .http.providers import MiddlewaresServiceProvider as MiddlewaresServiceProvider
from .http.providers import RoutersServiceProvider as RoutersServiceProvider
from .http.resources.collection import ResourceCollection as ResourceCollection
from .http.resources.resource import Resource as Resource
from .ioc import Resolve as Resolve
from .pagination.meta import PaginationMeta as PaginationMeta
from .pagination.paginator import Paginator as Paginator
from .support.facades.config import Config as Config

from .http.exceptions.resource_not_found import (
ResourceNotFoundException as ResourceNotFoundException,
)
2 changes: 1 addition & 1 deletion pyrannic/config/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pyrannic.bootstrap.instance_service_provider import InstanceServiceProvider
from pyrannic.config.repository import ConfigRepository
from pyrannic.contracts.config.configuration import ConfigurationInterface
from pyrannic.contracts.config.respository import ConfigRepositoryInterface
from pyrannic.contracts.config.repository import ConfigRepositoryInterface
from pyrannic.support.path import get_module_paths
from pyrannic.support.reflection import get_classes

Expand Down
2 changes: 1 addition & 1 deletion pyrannic/config/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from annotated_types import T

from pyrannic.contracts.config.respository import ConfigRepositoryInterface
from pyrannic.contracts.config.repository import ConfigRepositoryInterface
from pyrannic.support.collections.dot_dict import get, has, set


Expand Down
101 changes: 76 additions & 25 deletions pyrannic/container/container.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import inspect
from collections.abc import Callable
from contextlib import AsyncExitStack
import inspect
from types import FunctionType
from typing import Any, Awaitable, TypeVar, cast, get_args, get_origin

from fastapi import Request
from fastapi.concurrency import run_in_threadpool
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
SolvedDependency,
get_dependant,
solve_dependencies,
)
from fastapi.exceptions import RequestValidationError
from fastapi.types import DependencyCacheKey

Expand All @@ -17,13 +23,6 @@
)
from pyrannic.support.reflection import is_interface

from fastapi.dependencies.utils import (
get_dependant,
solve_dependencies,
SolvedDependency,
)
from fastapi.dependencies.models import Dependant

T = TypeVar("T")


Expand Down Expand Up @@ -200,10 +199,21 @@ def is_bound(self, abstract: str | type) -> bool:
or self.is_alias(abstract)
)

async def make(
self,
abstract: str | type[T],
*args: Any,
request: Request | None = None,
**kwargs: Any,
) -> T:
return await self.resolve(abstract, *args, request=request, **kwargs)

async def resolve(
self,
abstract: str | type[T],
*args: Any,
request: Request | None = None,
**kwargs: Any,
) -> T:
binding_key = self.get_alias(abstract)
concrete = self._get_contextual_concrete(binding_key)
Expand All @@ -218,7 +228,7 @@ async def resolve(
if not concrete:
concrete = self._get_concrete(abstract)

instance = concrete(self._app, request)
instance = concrete(self._app, request, *args, **kwargs)

if inspect.isawaitable(instance):
instance = await instance
Expand Down Expand Up @@ -291,8 +301,13 @@ def _get_concrete(self, abstract: str | type[T]) -> Callable[..., Any]:

return concrete

async def call(self, callback: type[T] | Callable[..., Any]) -> T:
return await self._resolve(callback, self._app)
async def call(
self,
callback: type[T] | Callable[..., Any],
*args: Any,
**kwargs: Any,
) -> T:
return await self._resolve(callback, self._app, *args, **kwargs)

def resolved(self, abstract: str | type[T]) -> bool:
abstract = self.get_alias(abstract)
Expand Down Expand Up @@ -350,13 +365,25 @@ def _get_closure(
self,
concrete: type[T],
) -> Callable[[ApplicationInterface, Request], Awaitable[T]]:
async def closure(app: ApplicationInterface, request: Request) -> T:
async def closure(
app: ApplicationInterface,
request: Request,
*args: Any,
**kwargs: Any,
) -> T:
origin = get_origin(concrete)

if origin is not None and inspect.isclass(origin):
return await self._resolve_generic(concrete, origin, app, request)
return await self._resolve_generic(
concrete,
origin,
app,
request,
*args,
**kwargs,
)

return await self._resolve(concrete, app, request)
return await self._resolve(concrete, app, request, *args, **kwargs)

return closure

Expand All @@ -380,12 +407,14 @@ async def _resolve_generic(
origin: type[T],
app: ApplicationInterface,
request: Request | None = None,
*args: Any,
**kwargs: Any,
) -> T:
# Add the __orig_class__ attribute to the origin class so that we can retrieve the generic type later.
# For example, if we have a generic class Repository[Model], we can retrieve the Model type later by accessing the __orig_class__ attribute.
# This is necessary because FastAPI's dependency injection system does not support generic types out of the box.
setattr(origin, "__orig_class__", generic)
instance = await self._resolve(origin, app, request)
instance = await self._resolve(origin, app, request, *args, **kwargs)

try:
setattr(instance, "__orig_class__", generic)
Expand All @@ -399,63 +428,85 @@ async def _resolve(
callback: type[T] | Callable[..., Any],
app: ApplicationInterface,
request: Request | None = None,
*args: Any,
**kwargs: Any,
) -> T:
dependant = get_dependant(path="/", call=callback, scope="function")

if not request:
async with AsyncExitStack() as context_manager:
dependencies = await self._solve_dependencies(
self._fallback_request(app, context_manager),
dependant,
)
request = self._fallback_request(app, context_manager)
dependencies = await self._solve_dependencies(request, dependant)

return await self._resolve_dependant(
dependant,
dependencies,
*args,
**kwargs,
)
else:
dependencies = await self._solve_dependencies(request, dependant)
return await self._resolve_dependant(dependant, dependencies, **kwargs)

return await self._resolve_dependant(
dependant,
dependencies,
*args,
**kwargs,
)

# https://stackoverflow.com/a/78279023
# https://github.com/fastapi/fastapi/discussions/7720
async def _resolve_dependant(
self,
dependant: Dependant,
dependencies: SolvedDependency,
*args: Any,
**kwargs: Any,
) -> Any:
assert dependant.call # For types

errors = self._validate_errors(dependencies.errors)
errors = self._validate_errors(dependencies.errors, *args, **kwargs)

if bool(errors):
raise RequestValidationError(errors)

if inspect.iscoroutinefunction(dependant.call):
result = await dependant.call(**dependencies.values, **kwargs)
result = await dependant.call(*args, **dependencies.values, **kwargs)
else:
result = await run_in_threadpool(
dependant.call,
*args,
**dependencies.values,
**kwargs,
)

return result

def _validate_errors(self, errors: list[Any]) -> list[Any]:
def _validate_errors(
self,
errors: list[Any],
*args: Any,
**kwargs: Any,
) -> list[Any]:
if not bool(errors):
return []

error_counter = 0
valid_errors: list[Any] = []
length = len(args)

for error in errors:
loc = error.get("loc", [])

if "kwargs" not in loc and "args" not in loc:
attr = loc[-1] if loc else None

if (
attr != "kwargs"
and attr != "args"
and attr not in kwargs
and length <= error_counter
):
valid_errors.append(error)
error_counter += 1

return valid_errors

Expand Down
6 changes: 4 additions & 2 deletions pyrannic/container/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from fastapi.params import Depends

from pyrannic.container.decorators.singleton import singleton
from pyrannic.container.decorators.scoped import scoped
from pyrannic.container.decorators.singleton import singleton
from pyrannic.contracts.http.request import RequestInterface


Expand All @@ -28,7 +28,9 @@ def __init__(
@classmethod
def wrap_dependency(cls, abstract: str | type) -> Callable[..., Any]:
async def dependency(request: RequestInterface) -> Any:
return cast(Any, await request.app.container.resolve(abstract, request))
return cast(
Any, await request.app.container.resolve(abstract, request=request)
)

return dependency

Expand Down
7 changes: 5 additions & 2 deletions pyrannic/contracts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from .application import ApplicationInterface as ApplicationInterface
from .config.configuration import ConfigurationInterface as ConfigurationInterface
from .config.respository import ConfigRepositoryInterface as ConfigRepositoryInterface
from .config.repository import ConfigRepositoryInterface as ConfigRepositoryInterface
from .container.container import ContainerInterface as ContainerInterface
from .database.connector import ConnectorInterface as ConnectorInterface
from .database.manager import DatabaseManagerInterface as DatabaseManagerInterface
from .http.resources.collection import (
ResourceCollectionInterface as ResourceCollectionInterface,
)
from .http.resources.resource import ResourceInterface as ResourceInterface
from .orm.async_repository import AsyncRepositoryInterface as AsyncRepositoryInterface
from .orm.repository import RepositoryInterface as RepositoryInterface
from .pagination.paginator import PaginatorInterface as PaginatorInterface
from .container.container import ContainerInterface as ContainerInterface
4 changes: 4 additions & 0 deletions pyrannic/contracts/container/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .container import ContainerInterface as ContainerInterface
from .contextual_binding_builder import (
ContextualBindingBuilderInterface as ContextualBindingBuilderInterface,
)
Loading
Loading