API scoped requests #186
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 18s
Build on push / core (push) Successful in 21s
Build on push / dependency (push) Successful in 14s
Build on push / api (push) Has been cancelled
Build on push / auth (push) Has been cancelled
Build on push / application (push) Has been cancelled
Build on push / database (push) Has been cancelled
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled

This commit is contained in:
2025-09-24 21:47:52 +02:00
parent 287f5e3149
commit b49f663ae0
9 changed files with 67 additions and 48 deletions

View File

@@ -6,8 +6,10 @@ from cpl.application import ApplicationBuilder
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.schema import AuthUser, Role from cpl.auth.schema import AuthUser, Role
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.console import Console
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.utils.cache import Cache from cpl.core.utils.cache import Cache
from custom.api.src.scoped_service import ScopedService
from service import PingService from service import PingService
@@ -23,6 +25,8 @@ def main():
builder.services.add_transient(PingService) builder.services.add_transient(PingService)
builder.services.add_module(api) builder.services.add_module(api)
builder.services.add_scoped(ScopedService)
builder.services.add_cache(AuthUser) builder.services.add_cache(AuthUser)
builder.services.add_cache(Role) builder.services.add_cache(Role)
@@ -40,6 +44,32 @@ def main():
user_cache = provider.get_service(Cache[AuthUser]) user_cache = provider.get_service(Cache[AuthUser])
role_cache = provider.get_service(Cache[Role]) role_cache = provider.get_service(Cache[Role])
if role_cache == user_cache:
raise Exception("Cache service is not working")
s1 = provider.get_service(ScopedService)
s2 = provider.get_service(ScopedService)
if s1.name == s2.name:
raise Exception("Scoped service is not working")
with provider.create_scope() as scope:
s3 = scope.get_service(ScopedService)
s4 = scope.get_service(ScopedService)
if s3.name != s4.name:
raise Exception("Scoped service is not working")
if s1.name == s3.name:
raise Exception("Scoped service is not working")
Console.write_line(
s1.name,
s2.name,
s3.name,
s4.name,
)
app.run() app.run()

View File

@@ -5,12 +5,17 @@ from starlette.responses import JSONResponse
from cpl.api import APILogger from cpl.api import APILogger
from cpl.api.router import Router from cpl.api.router import Router
from cpl.core.console import Console
from cpl.dependency import ServiceProvider
from custom.api.src.scoped_service import ScopedService
@Router.authenticate() @Router.authenticate()
# @Router.authorize(permissions=[Permissions.administrator]) # @Router.authorize(permissions=[Permissions.administrator])
# @Router.authorize(policies=["test"]) # @Router.authorize(policies=["test"])
@Router.get(f"/ping") @Router.get(f"/ping")
async def ping(r: Request, ping: PingService, logger: APILogger): async def ping(r: Request, ping: PingService, logger: APILogger, provider: ServiceProvider, scoped: ScopedService):
logger.info(f"Ping: {ping}") logger.info(f"Ping: {ping}")
Console.write_line(scoped.name)
return JSONResponse(ping.ping(r)) return JSONResponse(ping.ping(r))

View File

@@ -0,0 +1,14 @@
from cpl.core.console.console import Console
from cpl.core.utils.string import String
class ScopedService:
def __init__(self):
self._name = String.random(8)
@property
def name(self) -> str:
return self._name
def run(self):
Console.write_line(f"Im {self._name}")

View File

@@ -62,7 +62,7 @@ class Application(ApplicationABC):
root_scoped_service2 = self._services.get_service(ScopedService) root_scoped_service2 = self._services.get_service(ScopedService)
Console.write_line(root_scoped_service2) Console.write_line(root_scoped_service2)
if root_scoped_service != root_scoped_service2: if root_scoped_service == root_scoped_service2:
raise Exception("Root scoped service should be equal to root scoped service 2") raise Exception("Root scoped service should be equal to root scoped service 2")
test_settings = Configuration.get(TestSettings) test_settings = Configuration.get(TestSettings)

View File

@@ -30,7 +30,6 @@ from cpl.core.configuration import Configuration
from cpl.dependency.inject import inject from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
PolicyInput = Union[dict[str, PolicyResolver], Policy] PolicyInput = Union[dict[str, PolicyResolver], Policy]

View File

@@ -1,13 +0,0 @@
from cpl.api.abc import ASGIMiddleware
from cpl.dependency.service_provider import ServiceProvider
class ScopeMiddleware(ASGIMiddleware):
def __init__(self, app, provider: ServiceProvider):
ASGIMiddleware.__init__(self, app)
self._app = app
self._provider = provider
async def __call__(self, scope, receive, send):
with self._provider.create_scope():
await self._app(scope, receive, send)

View File

@@ -9,15 +9,18 @@ from starlette.types import Scope, Receive, Send
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.typing import TRequest from cpl.api.typing import TRequest
from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider
_request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None) _request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None)
class RequestMiddleware(ASGIMiddleware): class RequestMiddleware(ASGIMiddleware):
def __init__(self, app, logger: APILogger): def __init__(self, app, provider: ServiceProvider, logger: APILogger):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._provider = provider
self._logger = logger self._logger = logger
self._ctx_token = None self._ctx_token = None
@@ -27,7 +30,8 @@ class RequestMiddleware(ASGIMiddleware):
await self.set_request_data(request) await self.set_request_data(request)
try: try:
await self._app(scope, receive, send) with self._provider.create_scope():
inject(await self._app(scope, receive, send))
finally: finally:
await self.clean_request_data() await self.clean_request_data()

View File

@@ -10,7 +10,6 @@ def inject(f=None):
return functools.partial(inject) return functools.partial(inject)
if iscoroutinefunction(f): if iscoroutinefunction(f):
@functools.wraps(f) @functools.wraps(f)
async def async_inner(*args, **kwargs): async def async_inner(*args, **kwargs):
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider

View File

@@ -14,23 +14,9 @@ from cpl.dependency.service_lifetime import ServiceLifetimeEnum
class ServiceProvider: class ServiceProvider:
r"""Provider for the services def __init__(self, service_descriptors: list[ServiceDescriptor], is_scope: bool = False):
Parameter
---------
service_descriptors: list[:class:`cpl.dependency.service_descriptor.ServiceDescriptor`]
Descriptor of the service
config: :class:`cpl.core.configuration.configuration_abc.ConfigurationABC`
CPL Configuration
db_context: Optional[:class:`cpl.database.context.database_context_abc.DatabaseContextABC`]
Database representation
"""
def __init__(
self,
service_descriptors: list[ServiceDescriptor],
):
self._service_descriptors: list[ServiceDescriptor] = service_descriptors self._service_descriptors: list[ServiceDescriptor] = service_descriptors
self._is_scope = is_scope
def _find_service(self, service_type: type) -> Optional[ServiceDescriptor]: def _find_service(self, service_type: type) -> Optional[ServiceDescriptor]:
origin_type = typing.get_origin(service_type) or service_type origin_type = typing.get_origin(service_type) or service_type
@@ -63,7 +49,7 @@ class ServiceProvider:
return descriptor.implementation return descriptor.implementation
implementation = self._build_service(descriptor.service_type, origin_service_type=origin_service_type) implementation = self._build_service(descriptor.service_type, origin_service_type=origin_service_type)
if descriptor.lifetime == ServiceLifetimeEnum.singleton: if descriptor.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped):
descriptor.implementation = implementation descriptor.implementation = implementation
return implementation return implementation
@@ -81,7 +67,7 @@ class ServiceProvider:
implementation = self._build_service( implementation = self._build_service(
descriptor.service_type, origin_service_type=service_type, **kwargs descriptor.service_type, origin_service_type=service_type, **kwargs
) )
if descriptor.lifetime == ServiceLifetimeEnum.singleton: if descriptor.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped):
descriptor.implementation = implementation descriptor.implementation = implementation
implementations.append(implementation) implementations.append(implementation)
@@ -127,12 +113,10 @@ class ServiceProvider:
service_type = type(descriptor.implementation) service_type = type(descriptor.implementation)
else: else:
service_type = descriptor.service_type service_type = descriptor.service_type
break break
sig = signature(service_type.__init__) sig = signature(service_type.__init__)
params = self._build_by_signature(sig, origin_service_type) params = self._build_by_signature(sig, origin_service_type)
return service_type(*params, *args, **kwargs) return service_type(*params, *args, **kwargs)
@contextmanager @contextmanager
@@ -144,13 +128,12 @@ class ServiceProvider:
else: else:
scoped_descriptors.append(copy.deepcopy(d)) scoped_descriptors.append(copy.deepcopy(d))
scoped_provider = ServiceProvider(scoped_descriptors) scoped_provider = ServiceProvider(scoped_descriptors, is_scope=True)
with use_provider(scoped_provider): with use_provider(scoped_provider):
yield scoped_provider yield scoped_provider
def get_service(self, service_type: T, *args, **kwargs) -> Optional[R]: def get_service(self, service_type: T, *args, **kwargs) -> Optional[R]:
result = self._find_service(service_type) result = self._find_service(service_type)
if result is None: if result is None:
return None return None
@@ -158,9 +141,10 @@ class ServiceProvider:
return result.implementation return result.implementation
implementation = self._build_service(service_type, *args, **kwargs) implementation = self._build_service(service_type, *args, **kwargs)
if (
result.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped) if result.lifetime == ServiceLifetimeEnum.singleton:
): result.implementation = implementation
elif result.lifetime == ServiceLifetimeEnum.scoped and self._is_scope:
result.implementation = implementation result.implementation = implementation
return implementation return implementation
@@ -173,12 +157,9 @@ class ServiceProvider:
def get_services(self, service_type: T, *args, **kwargs) -> list[Optional[R]]: def get_services(self, service_type: T, *args, **kwargs) -> list[Optional[R]]:
implementations = [] implementations = []
if typing.get_origin(service_type) == list: if typing.get_origin(service_type) == list:
raise Exception(f"Invalid type {service_type}! Expected single type not list of type") raise Exception(f"Invalid type {service_type}! Expected single type not list of type")
implementations.extend(self._get_services(service_type)) implementations.extend(self._get_services(service_type))
return implementations return implementations
def get_service_types(self, service_type: Type[T]) -> list[Type[T]]: def get_service_types(self, service_type: Type[T]) -> list[Type[T]]: