From 25944782a7cc8d4bbd35eaad16ff7d52f84ceac1 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 28 Dec 2025 18:04:17 +0100 Subject: [PATCH 1/2] Adding more code documentation --- ellar/app/context.py | 45 ++-- ellar/app/factory.py | 29 ++- ellar/app/main.py | 44 +++- ellar/auth/decorators.py | 141 ++++++------ ellar/auth/guards/apikey.py | 18 ++ ellar/auth/guards/auth_required.py | 5 +- ellar/auth/handlers/model.py | 11 +- ellar/auth/identity.py | 4 + ellar/auth/interceptor.py | 15 ++ ellar/auth/middleware/auth.py | 9 +- ellar/auth/policy/base.py | 50 +++- ellar/auth/services/auth_schemes.py | 15 ++ ellar/auth/session/strategy.py | 21 ++ ellar/cache/backends/base.py | 16 +- ellar/cache/backends/local_cache.py | 4 + ellar/cache/backends/pylib_cache.py | 4 +- ellar/cache/backends/pymem_cache.py | 7 +- ellar/cache/backends/serializer.py | 12 + ellar/cache/decorator.py | 39 +++- ellar/cache/interface.py | 303 ++++++++++++++++--------- ellar/cache/make_key_decorator.py | 16 ++ ellar/cache/module.py | 4 + ellar/cache/schema.py | 11 + ellar/cache/service.py | 2 +- ellar/common/constants.py | 16 ++ ellar/common/converters.py | 4 + ellar/common/decorators/base.py | 11 + ellar/common/decorators/controller.py | 12 +- ellar/common/decorators/exception.py | 14 +- ellar/common/decorators/extra_args.py | 17 +- ellar/common/decorators/file.py | 12 +- ellar/common/decorators/guards.py | 16 +- ellar/common/decorators/html.py | 9 +- ellar/common/decorators/interceptor.py | 18 +- ellar/common/decorators/middleware.py | 22 +- ellar/common/decorators/modules.py | 14 +- ellar/common/decorators/serializer.py | 23 +- ellar/common/decorators/versioning.py | 10 +- ellar/common/responses/models/base.py | 2 +- ellar/common/shortcuts.py | 3 + ellar/events/build.py | 25 +- ellar/openapi/openapi_v3.py | 2 +- ellar/reflect/__init__.py | 5 + ellar/reflect/_reflect.py | 140 ++++++++++++ ellar/reflect/utils.py | 39 ++++ 45 files changed, 966 insertions(+), 273 deletions(-) diff --git a/ellar/app/context.py b/ellar/app/context.py index 2ec3ff6d..86659fa5 100644 --- a/ellar/app/context.py +++ b/ellar/app/context.py @@ -16,9 +16,12 @@ @ensure_build_context(app_ready=True) def ensure_available_in_providers(*items: t.Any) -> None: """ - Ensures that Providers at least belows to a particular module - :param items: - :return: + Ensures that Providers belong to a particular module. + + If a provider is not found in any existing module, it is added to the + application module and exported. + + :param items: List of providers to check. """ app_module = current_injector.tree_manager.get_app_module() @@ -43,9 +46,9 @@ def _(data: TreeData) -> bool: @ensure_build_context(app_ready=True) def use_authentication_schemes(*authentication: AuthenticationHandlerType) -> None: """ - Registered Authentication Handlers to the application - :param authentication: - :return: + Registers Authentication Handlers to the application. + + :param authentication: List of authentication handlers or schemes. """ __identity_scheme = current_injector.get(IIdentitySchemes) for auth in authentication: @@ -58,9 +61,9 @@ def use_exception_handler( *exception_handlers: IExceptionHandler, ) -> None: """ - Adds Application Exception Handlers - :param exception_handlers: IExceptionHandler - :return: + Adds Application Exception Handlers. + + :param exception_handlers: List of exception handlers to register. """ for exception_handler in exception_handlers: if exception_handler not in current_config.EXCEPTION_HANDLERS: @@ -78,12 +81,12 @@ def enable_versioning( **init_kwargs: t.Any, ) -> None: """ - Enables an Application Versioning scheme - :param schema: VersioningSchemes - :param version_parameter: versioning parameter lookup key. Default: 'version' - :param default_version: versioning default value. Default: None - :param init_kwargs: Other schema initialization keyword args. - :return: + Enables an Application Versioning scheme. + + :param schema: The versioning scheme to use (e.g., URL, Header, Query). + :param version_parameter: The parameter name for version lookup. Default: 'version'. + :param default_version: The default version to use if none is specified. Default: None. + :param init_kwargs: Additional initialization arguments for the versioning scheme. """ current_config.VERSIONING_SCHEME = schema.value( version_parameter=version_parameter, @@ -97,9 +100,9 @@ def use_global_guards( *guards: t.Union[GuardCanActivate, t.Type[GuardCanActivate]], ) -> None: """ - Registers Application Global Guards that affects all routes registered in ApplicationRouter - :param guards: - :return: None + Registers Application Global Guards that affect all routes registered in the ApplicationRouter. + + :param guards: List of guards to register globally. """ current_config.GLOBAL_GUARDS = list(current_config.GLOBAL_GUARDS) + list(guards) ensure_available_in_providers(*guards) @@ -110,9 +113,9 @@ def use_global_interceptors( *interceptors: t.Union[EllarInterceptor, t.Type[EllarInterceptor]], ) -> None: """ - Registers Application Global Interceptor that affects all routes registered in ApplicationRouter - :param interceptors: - :return: None + Registers Application Global Interceptors that affect all routes registered in the ApplicationRouter. + + :param interceptors: List of interceptors to register globally. """ current_config.GLOBAL_INTERCEPTORS = list( current_config.GLOBAL_INTERCEPTORS diff --git a/ellar/app/factory.py b/ellar/app/factory.py index 46f126d5..be7f86a1 100644 --- a/ellar/app/factory.py +++ b/ellar/app/factory.py @@ -34,7 +34,9 @@ class AppFactory: """ - Factory for creating Ellar Application + Factory for creating Ellar Application instances. + + Provides methods to create an application from a module, controller, or existing app module. """ @classmethod @@ -189,6 +191,22 @@ def create_app( config_module: t.Union[str, t.Dict, None] = None, injector: t.Optional[EllarInjector] = None, ) -> App: + """ + Creates an Ellar Application instance. + + :param controllers: List of controllers to register. + :param routers: List of routers to register. + :param providers: List of providers to register. + :param modules: List of modules to register. + :param template_folder: Path to the template folder. + :param base_directory: Path to the base directory. + :param static_folder: Name of the static folder. Default: "static". + :param global_guards: List of global guards to register. + :param commands: List of commands to register. + :param config_module: Configuration module or dictionary. + :param injector: Optional existing injector instance. + :return: Configured App instance. + """ module = Module( controllers=controllers, routers=routers, @@ -219,6 +237,15 @@ def create_from_app_module( injector: t.Optional[EllarInjector] = None, config_module: t.Union[str, t.Dict, None] = None, ) -> App: + """ + Creates an Ellar Application from an existing Application Module. + + :param module: The application module class. + :param global_guards: List of global guards to register. + :param injector: Optional existing injector instance. + :param config_module: Configuration module or dictionary. + :return: Configured App instance. + """ app = cls._create_app( module, config_module=config_module, diff --git a/ellar/app/main.py b/ellar/app/main.py index a1b377b3..ef8b2943 100644 --- a/ellar/app/main.py +++ b/ellar/app/main.py @@ -44,6 +44,10 @@ class App: + """ + The main Ellar Application class. + """ + def __init__( self, config: "Config", @@ -117,6 +121,11 @@ def get_interceptors( async def use_global_guards( self, *guards: t.Union["GuardCanActivate", t.Type["GuardCanActivate"]] ) -> None: + """ + Registers global guards for the application. + + :param guards: Guards to register. + """ async with self.with_injector_context(): use_global_guards(*guards) @@ -125,6 +134,11 @@ async def use_global_guards( async def use_global_interceptors( self, *interceptors: t.Union[EllarInterceptor, t.Type[EllarInterceptor]] ) -> None: + """ + Registers global interceptors for the application. + + :param interceptors: Interceptors to register. + """ async with self.with_injector_context(): use_global_interceptors(*interceptors) @@ -173,11 +187,13 @@ def request_context( send: TSend = constants.empty_send, ) -> HttpRequestConnectionContext: """ - Create an RequestContext during request and provides instance for `current_connection`. - e.g + Creates a RequestContext during a request and provides an instance for `current_connection`. + Example: + ```python request = current_connection.switch_http_connection().get_request() websocket = current_connection.switch_to_websocket().get_client() + ``` """ context_factory = self.injector.get(IHostContextFactory) return HttpRequestConnectionContext( @@ -226,6 +242,14 @@ async def enable_versioning( default_version: t.Optional[str] = None, **init_kwargs: t.Any, ) -> None: + """ + Enables API versioning for the application. + + :param schema: Versioning scheme to use. + :param version_parameter: Parameter name for versioning. + :param default_version: Default version if none specified. + :param init_kwargs: Additional arguments for the versioning scheme. + """ async with self.with_injector_context(): enable_versioning( schema, @@ -262,6 +286,11 @@ async def add_exception_handler( self, *exception_handlers: IExceptionHandler, ) -> None: + """ + Registers global exception handlers. + + :param exception_handlers: Exception handlers to register. + """ async with self.with_injector_context(): use_exception_handler(*exception_handlers) @@ -274,6 +303,11 @@ def reflector(self) -> Reflector: async def add_authentication_schemes( self, *authentication: AuthenticationHandlerType ) -> None: + """ + Registers authentication schemes for the application. + + :param authentication: Authentication schemes to register. + """ async with self.with_injector_context(): use_authentication_schemes(*authentication) @@ -319,7 +353,11 @@ def url_for(context: t.Dict, name: str, **path_params: t.Any) -> URL: return jinja_env def setup_jinja_environment(self) -> Environment: - """Sets up Jinja2 Environment and adds it to DI""" + """ + Sets up the Jinja2 Environment and adds it to the dependency injection container. + + :return: Configured Jinja2 Environment. + """ jinja_environment = self._create_jinja_environment() self.injector.tree_manager.get_app_module().add_provider( diff --git a/ellar/auth/decorators.py b/ellar/auth/decorators.py index 57c199da..3ae6b29a 100644 --- a/ellar/auth/decorators.py +++ b/ellar/auth/decorators.py @@ -13,26 +13,22 @@ def CheckPolicies(*policies: t.Union[str, PolicyType]) -> t.Callable: """ Applies policy requirements to a controller or route function. - This decorator allows you to specify one or more policies that must be satisfied - for the user to access the decorated endpoint. Policies can be either string - identifiers or PolicyType objects. + This decorator enforces one or more policies that must be satisfied for the user to access the decorated endpoint. + Policies can be specified as string identifiers (resolved via provider) or `PolicyType` objects. Example: - ```python - @Controller() - class UserController: - @CheckPolicies('admin', RequireRole('manager')) - def get_sensitive_data(self): - return {'data': 'sensitive information'} - ``` - - Args: - *policies: Variable number of policy requirements. Can be strings or PolicyType objects. - - String policies are resolved using the policy provider - - PolicyType objects are evaluated directly - - Returns: - A decorator function that applies the policy requirements. + ```python + @Controller() + class UserController: + @CheckPolicies('admin', RequireRole('manager')) + def get_sensitive_data(self): + return {'data': 'sensitive information'} + ``` + + :param policies: Variable number of policy requirements. + - String policies are resolved using the service provider. + - `PolicyType` objects are evaluated directly. + :return: A decorator function that applies the policy requirements. """ def _decorator(target: t.Callable) -> t.Union[t.Callable, t.Any]: @@ -46,23 +42,22 @@ def Authorize() -> t.Callable: """ Enables authorization checks for a controller or route function. - This decorator adds the AuthorizationInterceptor which performs two main checks: - 1. Verifies that the user is authenticated - 2. Validates any policy requirements specified using @CheckPolicies + This decorator registers the `AuthorizationInterceptor`, which performs two primary checks: + 1. Verifies that the user is authenticated. + 2. Validates any policy requirements specified using `@CheckPolicies`. Example: - ```python - @Controller() - @Authorize() # Enable authorization for all routes in controller - class SecureController: - @get('/') - @CheckPolicies('admin') # Require admin policy - def secure_endpoint(self): - return {'message': 'secure data'} - ``` - - Returns: - A decorator function that enables authorization checks. + ```python + @Controller() + @Authorize() # Enable authorization for all routes in the controller + class SecureController: + @get('/') + @CheckPolicies('admin') # Require admin policy for this specific route + def secure_endpoint(self): + return {'message': 'secure data'} + ``` + + :return: A decorator function that enables authorization checks. """ return set_meta(constants.ROUTE_INTERCEPTORS, [AuthorizationInterceptor]) @@ -75,32 +70,29 @@ def AuthenticationRequired( """ Requires authentication for accessing a controller or route function. - This decorator adds the AuthenticatedRequiredGuard which ensures that requests - are authenticated before they can access the protected resource. + This decorator adds the `AuthenticatedRequiredGuard` to ensure that requests are authenticated + before accessing the protected resource. Example: - ```python - @Controller() - class UserController: - @get('/profile') - @AuthenticationRequired('jwt') # Require JWT authentication - def get_profile(self): - return {'user': 'data'} - - @get('/public') - @AuthenticationRequired(openapi_scope=['read:public']) - def public_data(self): - return {'public': 'data'} - ``` - - Args: - authentication_scheme: Optional name of the authentication scheme to use. - This should match the scheme name defined in your authentication setup. - openapi_scope: Optional list of OpenAPI security scopes required for the endpoint. - These scopes will be reflected in the OpenAPI documentation. - - Returns: - A decorator function that enforces authentication requirements. + ```python + @Controller() + class UserController: + @get('/profile') + @AuthenticationRequired('jwt') # Require JWT authentication + def get_profile(self): + return {'user': 'data'} + + @get('/public') + @AuthenticationRequired(openapi_scope=['read:public']) + def public_data(self): + return {'public': 'data'} + ``` + + :param authentication_scheme: Optional name of the authentication scheme to use. + This must match a scheme defined in the application's authentication setup. + :param openapi_scope: Optional list of OpenAPI security scopes required for the endpoint. + These scopes are reflected in the OpenAPI documentation. + :return: A decorator function that enforces authentication requirements. """ if callable(authentication_scheme): return set_meta(constants.GUARDS_KEY, [AuthenticatedRequiredGuard(None, [])])( @@ -117,26 +109,25 @@ def SkipAuth() -> t.Callable: """ Marks a controller or route function to skip authentication checks. - This decorator is useful when you have a controller with @AuthenticationRequired - but want to exclude specific routes from the authentication requirement. + This decorator is useful for excluding specific routes from authentication requirements + applied at the controller level (e.g., via `@AuthenticationRequired`). Example: - ```python - @Controller() - @AuthenticationRequired() # Require auth for all routes - class UserController: - @get('/private') - def private_data(self): # This requires auth - return {'private': 'data'} - - @get('/public') - @SkipAuth() # This endpoint skips auth - def public_data(self): - return {'public': 'data'} - ``` - - Returns: - A decorator function that marks the endpoint to skip authentication. + ```python + @Controller() + @AuthenticationRequired() # Require auth for all routes by default + class UserController: + @get('/private') + def private_data(self): # Inherits auth requirement + return {'private': 'data'} + + @get('/public') + @SkipAuth() # Overrides controller-level auth requirement + def public_data(self): + return {'public': 'data'} + ``` + + :return: A decorator function that marks the endpoint to skip authentication. """ return set_meta( diff --git a/ellar/auth/guards/apikey.py b/ellar/auth/guards/apikey.py index 550eb9dd..bc466854 100644 --- a/ellar/auth/guards/apikey.py +++ b/ellar/auth/guards/apikey.py @@ -8,6 +8,12 @@ class GuardAPIKeyQuery(APIKeyQuery, GuardAuthMixin, ABC): + """ + Abstract base class for API Key authentication via query parameters. + + Subclasses must implement `authentication_handler` to validate the key. + """ + openapi_in: str = "query" exception_class = APIException @@ -21,6 +27,12 @@ async def authentication_handler( class GuardAPIKeyCookie(APIKeyCookie, GuardAuthMixin, ABC): + """ + Abstract base class for API Key authentication via cookies. + + Subclasses must implement `authentication_handler` to validate the key. + """ + openapi_in: str = "cookie" exception_class = APIException @@ -34,6 +46,12 @@ async def authentication_handler( class GuardAPIKeyHeader(APIKeyHeader, GuardAuthMixin, ABC): + """ + Abstract base class for API Key authentication via headers. + + Subclasses must implement `authentication_handler` to validate the key. + """ + openapi_in: str = "header" exception_class = APIException diff --git a/ellar/auth/guards/auth_required.py b/ellar/auth/guards/auth_required.py index aa479fe9..0e10ce8c 100644 --- a/ellar/auth/guards/auth_required.py +++ b/ellar/auth/guards/auth_required.py @@ -10,7 +10,10 @@ class AuthenticatedRequiredGuard(GuardCanActivate): """ - This guard will check if the user is authenticated and also allow you to define the authentication scheme and openapi scope. + Guard that enforces authentication for a route. + + It checks if the user is authenticated and optionally validates against a specific authentication scheme. + It acts as a bridge between the `@AuthenticationRequired` decorator and the execution context. """ status_code = starlette.status.HTTP_401_UNAUTHORIZED diff --git a/ellar/auth/handlers/model.py b/ellar/auth/handlers/model.py index 9438ea3e..efb2f9fa 100644 --- a/ellar/auth/handlers/model.py +++ b/ellar/auth/handlers/model.py @@ -5,6 +5,13 @@ class BaseAuthenticationHandler(ABC): + """ + Base class for all authentication handlers. + + To create a custom authentication scheme, inherit from this class and implement the `authenticate` method. + You must also define a `scheme` attribute (e.g., 'bearer', 'basic', 'api-key'). + """ + scheme: str def __init_subclass__(cls, **kwargs: str) -> None: @@ -17,7 +24,9 @@ def openapi_security_scheme(cls) -> t.Optional[t.Dict]: @abstractmethod async def authenticate(self, context: IHostContext) -> t.Optional[Identity]: - """Authenticate Action goes here""" + """ + authenticate the request and return an Identity or None + """ AuthenticationHandlerType = t.Union[ diff --git a/ellar/auth/identity.py b/ellar/auth/identity.py index 9ee053bc..bf490c6b 100644 --- a/ellar/auth/identity.py +++ b/ellar/auth/identity.py @@ -4,6 +4,10 @@ class UserIdentity(Identity): + """ + Represents the identity of an authenticated user. + """ + roles: t.Any first_name: t.Optional[str] last_name: t.Optional[str] diff --git a/ellar/auth/interceptor.py b/ellar/auth/interceptor.py index 93ec6673..9ca4ae8a 100644 --- a/ellar/auth/interceptor.py +++ b/ellar/auth/interceptor.py @@ -12,6 +12,11 @@ @injectable class AuthorizationInterceptor(EllarInterceptor): + """ + Interceptor responsible for handling request authorization. + Verifies user authentication and evaluates policies required for route access. + """ + status_code = status.HTTP_403_FORBIDDEN exception_class = APIException @@ -21,6 +26,9 @@ class AuthorizationInterceptor(EllarInterceptor): def get_route_handler_policy( self, context: IExecutionContext ) -> t.Optional[t.List[t.Union[PolicyType, str]]]: + """ + Retrieves policy requirements for the current route handler. + """ return context.get_app().reflector.get_all_and_override( POLICY_KEYS, context.get_handler(), context.get_class() ) @@ -35,6 +43,13 @@ def _get_policy_instance( async def intercept( self, context: IExecutionContext, next_interceptor: t.Callable[..., t.Coroutine] ) -> t.Any: + """ + Intercepts the request to enforce authorization logic. + + - Checks if `@SkipAuth` is present. + - Verifies if the user is authenticated. + - Evaluates any registered policies. + """ skip_auth = reflector.get_all_and_override( constants.SKIP_AUTH, context.get_handler(), context.get_class() ) diff --git a/ellar/auth/middleware/auth.py b/ellar/auth/middleware/auth.py index a4551502..bdff569c 100644 --- a/ellar/auth/middleware/auth.py +++ b/ellar/auth/middleware/auth.py @@ -9,6 +9,13 @@ class IdentityMiddleware: + """ + Middleware that manages user identity for requests. + + It initializes the execution context and attempts to authenticate the user using the configured identity service. + If authentication fails or is not required, an `AnonymousIdentity` is assigned. + """ + def __init__( self, app: ASGIApp, @@ -43,7 +50,7 @@ async def __call__(self, scope: TScope, receive: TReceive, send: TSend) -> None: def is_static(self, scope: TScope) -> bool: """ - Check is the request is for a static file + Checks if the current request is for a static file. """ return self._path_regex.match(scope["path"]) is not None diff --git a/ellar/auth/policy/base.py b/ellar/auth/policy/base.py index 2b299b25..f618b959 100644 --- a/ellar/auth/policy/base.py +++ b/ellar/auth/policy/base.py @@ -7,15 +7,19 @@ class DefaultRequirementType(AttributeDict): """ - Stores Policy Requirement Arguments in `arg_[n]` value - example: - class MyPolicyHandler(PolicyWithRequirement): - ... - - policy = MyPolicyHandler['req1', 'req2', 'req2'] - policy.requirement.arg_1 = 'req1' - policy.requirement.arg_2 = 'req2' - policy.requirement.arg_3 = 'req2' + Standard container for Policy Requirement Arguments. + + Arguments passed to a policy are stored in `arg_[n]` keys. + + Example: + ```python + class MyPolicyHandler(PolicyWithRequirement): + ... + + policy = MyPolicyHandler['req1', 'req2'] + # policy.requirement.arg_1 == 'req1' + # policy.requirement.arg_2 == 'req2' + ``` """ def __init__(self, *args: t.Any) -> None: @@ -50,15 +54,33 @@ class PolicyMetaclass(_PolicyOperandMixin, ABCMeta): class Policy(ABC, _PolicyOperandMixin, metaclass=PolicyMetaclass): + """ + Abstract base class for all Policies. + + Policies are used to check permissions or conditions for accessing a route. + They can be combined using logical operators (&, |, ~). + """ + @abstractmethod async def handle(self, context: IExecutionContext) -> bool: - """Run Policy Actions and return true or false""" + """ + Executes the policy check. + + :param context: The execution context. + :return: True if the policy is satisfied, False otherwise. + """ class PolicyWithRequirement( Policy, ABC, ): + """ + Base class for policies that accept parameters or requirements. + + Example: `MyPolicy['admin', 'manager']` + """ + __requirements__: t.Dict[int, "Policy"] = {} requirement_type: t.Type = DefaultRequirementType @@ -66,7 +88,13 @@ class PolicyWithRequirement( @abstractmethod @t.no_type_check async def handle(self, context: IExecutionContext, requirement: t.Any) -> bool: - """Handle Policy Action""" + """ + Executes the policy check with the provided requirement. + + :param context: The execution context. + :param requirement: The requirement data (instance of `requirement_type`). + :return: True if the policy is satisfied. + """ def __class_getitem__(cls, parameters: t.Any) -> "Policy": _parameters = parameters if isinstance(parameters, tuple) else (parameters,) diff --git a/ellar/auth/services/auth_schemes.py b/ellar/auth/services/auth_schemes.py index 2a17e170..3e951549 100644 --- a/ellar/auth/services/auth_schemes.py +++ b/ellar/auth/services/auth_schemes.py @@ -6,6 +6,10 @@ class AppIdentitySchemes(IIdentitySchemes): + """ + Manages the collection of registered authentication schemes for the application. + """ + __slots__ = ("_authentication_schemes",) def __init__(self) -> None: @@ -14,11 +18,19 @@ def __init__(self) -> None: def add_authentication( self, authentication_scheme: AuthenticationHandlerType ) -> None: + """ + Registers a new authentication scheme. + """ self._authentication_schemes[authentication_scheme.scheme] = ( authentication_scheme ) def find_authentication_scheme(self, scheme: str) -> AuthenticationHandlerType: + """ + Retrieves an authentication scheme by name. + + :raises RuntimeError: If the scheme is not found. + """ try: return self._authentication_schemes[scheme] except KeyError as ex: @@ -29,5 +41,8 @@ def find_authentication_scheme(self, scheme: str) -> AuthenticationHandlerType: def get_authentication_schemes( self, ) -> t.Generator[AuthenticationHandlerType, t.Any, t.Any]: + """ + Yields all registered authentication schemes. + """ for _, v in self._authentication_schemes.items(): yield v diff --git a/ellar/auth/session/strategy.py b/ellar/auth/session/strategy.py index 2119a1c2..a047c549 100644 --- a/ellar/auth/session/strategy.py +++ b/ellar/auth/session/strategy.py @@ -20,6 +20,13 @@ @injectable class SessionClientStrategy(SessionStrategy): + """ + Implements a client-side session strategy using signed cookies. + + This strategy stores session data directly in the client's browser cookies, + signed with a secret key to prevent tampering. + """ + def __init__(self, config: Config) -> None: self._signer = itsdangerous.TimestampSigner(str(config.SECRET_KEY)) self.config = config @@ -42,6 +49,13 @@ def serialize_session( session: t.Union[str, SessionCookieObject], config: t.Optional[SessionCookieOption] = None, ) -> str: + """ + Serializes the session object into a signed string suitable for a cookie value. + + :param session: The session data to serialize. + :param config: Optional cookie configuration overrides. + :return: A serialized and signed session string. + """ session_config = config or self._session_config if isinstance(session, SessionCookieObject): data = b64encode(json.dumps(dict(session)).encode("utf-8")) @@ -58,6 +72,13 @@ def deserialize_session( session_data: t.Optional[str], config: t.Optional[SessionCookieOption] = None, ) -> SessionCookieObject: + """ + Deserializes a session string from a cookie into a SessionCookieObject. + + :param session_data: The signed session string from the cookie. + :param config: Optional cookie configuration overrides. + :return: A SessionCookieObject containing the session data, or an empty object if validation fails. + """ session_config = config or self._session_config if session_data: data = session_data.encode("utf-8") diff --git a/ellar/cache/backends/base.py b/ellar/cache/backends/base.py index 6171a8ad..7c7a6efd 100644 --- a/ellar/cache/backends/base.py +++ b/ellar/cache/backends/base.py @@ -56,9 +56,15 @@ def decr(self, key: str, delta: int = 1, version: t.Optional[str] = None) -> int return t.cast(int, result) def close(self, **kwargs: t.Any) -> None: - """Many clients don't clean up connections properly.""" + """ + Close the cache connection. + Many clients don't clean up connections properly. + """ def clear(self) -> None: + """ + Clear the cache. + """ self._cache_client.flush_all() @@ -115,10 +121,16 @@ async def touch_async( return bool(result) async def close_async(self, **kwargs: t.Any) -> None: - # Many clients don't clean up connections properly. + """ + Close the cache connection asynchronously. + Many clients don't clean up connections properly. + """ await self.executor(self._cache_client.disconnect_all) async def clear_async(self) -> None: + """ + Clear the cache asynchronously. + """ await self.executor(self._cache_client.flush_all) def validate_key(self, key: str) -> None: diff --git a/ellar/cache/backends/local_cache.py b/ellar/cache/backends/local_cache.py index 13e43097..e3855785 100644 --- a/ellar/cache/backends/local_cache.py +++ b/ellar/cache/backends/local_cache.py @@ -52,6 +52,10 @@ def decr(self, key: str, delta: int = 1, version: t.Optional[str] = None) -> int class LocalMemCacheBackend(_LocalMemCacheBackendSync, BaseCacheBackend): + """ + A thread-safe in-memory cache backend. + """ + pickle_protocol = pickle.HIGHEST_PROTOCOL def __init__(self, **kwargs: t.Any) -> None: diff --git a/ellar/cache/backends/pylib_cache.py b/ellar/cache/backends/pylib_cache.py index ed083cab..a66184f4 100644 --- a/ellar/cache/backends/pylib_cache.py +++ b/ellar/cache/backends/pylib_cache.py @@ -15,7 +15,9 @@ class PyLibMCCacheBackend(BasePylibMemcachedCache): - """An implementation of a cache binding using pylibmc""" + """ + An implementation of a cache binding using pylibmc. + """ MEMCACHE_CLIENT = Client diff --git a/ellar/cache/backends/pymem_cache.py b/ellar/cache/backends/pymem_cache.py index 57fce7ab..8f8bccf2 100644 --- a/ellar/cache/backends/pymem_cache.py +++ b/ellar/cache/backends/pymem_cache.py @@ -15,7 +15,8 @@ class Client(HashClient): - """pymemcache client. + """ + Pymemcache client. Customize pymemcache behavior as python-memcached (default backend)'s one. """ @@ -46,7 +47,9 @@ def disconnect_all(self) -> None: class PyMemcacheCacheBackend(BasePylibMemcachedCache): - """An implementation of a cache binding using pymemcache.""" + """ + An implementation of a cache binding using pymemcache. + """ MEMCACHE_CLIENT = Client diff --git a/ellar/cache/backends/serializer.py b/ellar/cache/backends/serializer.py index 8c4ce872..9878399e 100644 --- a/ellar/cache/backends/serializer.py +++ b/ellar/cache/backends/serializer.py @@ -8,10 +8,16 @@ class ICacheSerializer(ABC): @abstractmethod def load(self, data: t.Any) -> t.Any: # pragma: no cover + """ + Load data from cache. + """ ... @abstractmethod def dumps(self, data: t.Any) -> t.Any: # pragma: no cover + """ + Dump data to cache. + """ ... @@ -20,12 +26,18 @@ def __init__(self, protocol: t.Optional[int] = None) -> None: self._protocol = protocol or self.default_protocol def load(self, data: t.Any) -> t.Any: + """ + Load data from cache. + """ try: return int(data) except ValueError: return pickle.loads(data) def dumps(self, data: t.Any) -> t.Any: + """ + Dump data to cache. + """ # Only skip pickling for integers, an int subclasses as bool should be # pickled. if isinstance(data, int): diff --git a/ellar/cache/decorator.py b/ellar/cache/decorator.py index 28e4ac08..156d99a8 100644 --- a/ellar/cache/decorator.py +++ b/ellar/cache/decorator.py @@ -15,6 +15,10 @@ @dataclasses.dataclass class RouteCacheOptions: + """ + Cache Options for Route handlers + """ + ttl: t.Union[int, float] key_prefix: str make_key_callback: t.Callable[[IExecutionContext, str], str] @@ -75,14 +79,33 @@ def Cache( make_key_callback: t.Optional[t.Callable[[IExecutionContext, str], str]] = None, ) -> t.Callable: """ - =========CONTROLLER AND ROUTE FUNCTION DECORATOR ============== - - :param ttl: the time to live - :param key_prefix: cache key prefix - :param version: will be used in constructing the key - :param backend: Cache Backend to use. Default is `default` - :param make_key_callback: Key dynamic construct. - :return: TCallable + Decorates a controller or route function to cache its response. + + :param ttl: The time-to-live for the cache in seconds. + :param key_prefix: A prefix to identify the cache key. + :param version: A version string to be used in constructing the key. + :param backend: The name of the cache backend to use. Defaults to `default`. + :param make_key_callback: A callable to dynamically construct the cache key. + :return: A callable decorator. + + Examples: + --------- + ```python + from ellar.common import get, Controller + from ellar.cache import Cache + + @Controller + class MyController: + @get("/cached-route") + @Cache(ttl=60, key_prefix="my_route") + def cached_route(self): + return {"message": "This response is cached for 60 seconds"} + + @get("/dynamic-key") + @Cache(ttl=300, make_key_callback=lambda ctx, prefix: f"{prefix}:{ctx.switch_to_http_connection().query_params['id']}") + def dynamic_key_route(self, id: int): + return {"id": id, "message": "Cached based on query param 'id'"} + ``` """ def _wraps(func: t.Callable) -> t.Callable: diff --git a/ellar/cache/interface.py b/ellar/cache/interface.py index be844b5c..c266fc63 100644 --- a/ellar/cache/interface.py +++ b/ellar/cache/interface.py @@ -8,19 +8,22 @@ class IBaseCacheBackendAsync(ABC): @abstractmethod async def get_async(self, key: str, version: t.Optional[str] = None) -> t.Any: - """Look up key in the cache and return the value for it. - :param key: the key to be looked up. - :param version: the version for the key - :returns: The value if it exists and is readable, else ``None``. + """ + Look up key in the cache and return the value for it. + + :param key: The key to be looked up. + :param version: The version for the key. + :return: The value if it exists and is readable, else ``None``. """ @abstractmethod async def delete_async(self, key: str, version: t.Optional[str] = None) -> bool: - """Delete `key` from the cache. - :param key: the key to delete. - :param version: the version for the key - :returns: Whether the key existed and has been deleted. - :rtype: boolean + """ + Delete `key` from the cache. + + :param key: The key to delete. + :param version: The version for the key. + :return: Whether the key existed and has been deleted. """ @abstractmethod @@ -31,18 +34,17 @@ async def set_async( ttl: t.Union[float, int, None] = None, version: t.Optional[str] = None, ) -> bool: - """Add a new key/value to the cache (overwrites value, if key already - exists in the cache). - :param key: the key to set - :param value: the value for the key - :param version: the version for the key - :param ttl: the cache ttl for the key in seconds (if not - specified, it uses the default ttl). A ttl of - 0 indicates that the cache never expires. - :returns: ``True`` if key has been updated, ``False`` for backend - errors. Pickling errors, however, will raise a subclass of - ``pickle.PickleError``. - :rtype: boolean + """ + Add a new key/value to the cache (overwrites value, if key already exists in the cache). + + :param key: The key to set. + :param value: The value to be cached. + :param ttl: The cache time-to-live for the key in seconds. + If not specified, the default TTL is used. + A TTL of 0 indicates that the cache never expires. + :param version: The version for the key. + :return: ``True`` if key has been updated, ``False`` for backend errors. + Pickling errors, however, will raise a subclass of ``pickle.PickleError``. """ @abstractmethod @@ -53,14 +55,22 @@ async def touch_async( version: t.Optional[str] = None, ) -> bool: """ - Update the key's expiry time using ttl. Return True if successful - or False if the key does not exist. + Update the key's expiry time using ttl. + + :param key: The key to update. + :param ttl: The new time-to-live. + :param version: The version for the key. + :return: `True` if successful or `False` if the key does not exist. """ @abstractmethod async def has_key_async(self, key: str, version: t.Optional[str] = None) -> bool: """ Return True if the key is in the cache and has not expired. + + :param key: The key to check. + :param version: The version for the key. + :return: `True` if key is found and not expired, else `False`. """ @abstractmethod @@ -68,7 +78,12 @@ async def incr_async( self, key: str, delta: int = 1, version: t.Optional[str] = None ) -> int: """ - Increments the number stored at key by one. If the key does not exist, it is set to 0 + Increments the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to increment. + :param delta: The amount to increment by. + :param version: The version for the key. + :return: The new value. """ @abstractmethod @@ -76,26 +91,34 @@ async def decr_async( self, key: str, delta: int = 1, version: t.Optional[str] = None ) -> int: """ - Decrements the number stored at key by one. If the key does not exist, it is set to 0 + Decrements the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to decrement. + :param delta: The amount to decrement by. + :param version: The version for the key. + :return: The new value. """ class IBaseCacheBackendSync(ABC): @abstractmethod def get(self, key: str, version: t.Optional[str] = None) -> t.Any: - """Look up key in the cache and return the value for it. - :param key: the key to be looked up. - :param version: the version for the key - :returns: The value if it exists and is readable, else ``None``. + """ + Look up key in the cache and return the value for it. + + :param key: The key to be looked up. + :param version: The version for the key. + :return: The value if it exists and is readable, else ``None``. """ @abstractmethod def delete(self, key: str, version: t.Optional[str] = None) -> bool: - """Delete `key` from the cache. - :param key: the key to delete. - :param version: the version for the key - :returns: Whether the key existed and has been deleted. - :rtype: boolean + """ + Delete `key` from the cache. + + :param key: The key to delete. + :param version: The version for the key. + :return: Whether the key existed and has been deleted. """ @abstractmethod @@ -106,18 +129,17 @@ def set( ttl: t.Union[float, int, None] = None, version: t.Optional[str] = None, ) -> bool: - """Add a new key/value to the cache (overwrites value, if key already - exists in the cache). - :param key: the key to set - :param value: the value for the key - :param version: the version for the key - :param ttl: the cache ttl for the key in seconds (if not - specified, it uses the default ttl). A ttl of - 0 indicates that the cache never expires. - :returns: ``True`` if key has been updated, ``False`` for backend - errors. Pickling errors, however, will raise a subclass of - ``pickle.PickleError``. - :rtype: boolean + """ + Add a new key/value to the cache (overwrites value, if key already exists in the cache). + + :param key: The key to set. + :param value: The value to be cached. + :param ttl: The cache time-to-live for the key in seconds. + If not specified, the default TTL is used. + A TTL of 0 indicates that the cache never expires. + :param version: The version for the key. + :return: ``True`` if key has been updated, ``False`` for backend errors. + Pickling errors, however, will raise a subclass of ``pickle.PickleError``. """ @abstractmethod @@ -128,26 +150,44 @@ def touch( version: t.Optional[str] = None, ) -> bool: """ - Update the key's expiry time using ttl. Return True if successful - or False if the key does not exist. + Update the key's expiry time using ttl. + + :param key: The key to update. + :param ttl: The new time-to-live. + :param version: The version for the key. + :return: `True` if successful or `False` if the key does not exist. """ @abstractmethod def has_key(self, key: str, version: t.Optional[str] = None) -> bool: """ Return True if the key is in the cache and has not expired. + + :param key: The key to check. + :param version: The version for the key. + :return: `True` if key is found and not expired, else `False`. """ @abstractmethod def incr(self, key: str, delta: int = 1, version: t.Optional[str] = None) -> int: """ - Increments the number stored at key by one. If the key does not exist, it is set to 0 + Increments the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to increment. + :param delta: The amount to increment by. + :param version: The version for the key. + :return: The new value. """ @abstractmethod def decr(self, key: str, delta: int = 1, version: t.Optional[str] = None) -> int: """ - Decrements the number stored at key by one. If the key does not exist, it is set to 0 + Decrements the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to decrement. + :param delta: The amount to decrement by. + :param version: The version for the key. + :return: The new value. """ @@ -156,23 +196,26 @@ class ICacheServiceSync(ABC): def get( self, key: str, version: t.Optional[str] = None, backend: t.Optional[str] = None ) -> t.Any: - """Look up key in the cache and return the value for it. - :param key: the key to be looked up. - :param version: the version for the key - :param backend: a backend service name - :returns: The value if it exists and is readable, else ``None``. + """ + Look up key in the cache and return the value for it. + + :param key: The key to be looked up. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The value if it exists and is readable, else ``None``. """ @abstractmethod def delete( self, key: str, version: t.Optional[str] = None, backend: t.Optional[str] = None ) -> bool: - """Delete `key` from the cache. - :param key: the key to delete. - :param version: the version for the key - :param backend: a backend service name - :returns: Whether the key existed and has been deleted. - :rtype: boolean + """ + Delete `key` from the cache. + + :param key: The key to delete. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: Whether the key existed and has been deleted. """ @abstractmethod @@ -184,19 +227,18 @@ def set( version: t.Optional[str] = None, backend: t.Optional[str] = None, ) -> bool: - """Add a new key/value to the cache (overwrites value, if key already - exists in the cache). - :param key: the key to set - :param value: the value for the key - :param version: the version for the key - :param backend: a backend service name - :param ttl: the cache ttl for the key in seconds (if not - specified, it uses the default ttl). A ttl of - 0 indicates that the cache never expires. - :returns: ``True`` if key has been updated, ``False`` for backend - errors. Pickling errors, however, will raise a subclass of - ``pickle.PickleError``. - :rtype: boolean + """ + Add a new key/value to the cache (overwrites value, if key already exists in the cache). + + :param key: The key to set. + :param value: The value to be cached. + :param ttl: The cache time-to-live for the key in seconds. + If not specified, the default TTL is used. + A TTL of 0 indicates that the cache never expires. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: ``True`` if key has been updated, ``False`` for backend errors. + Pickling errors, however, will raise a subclass of ``pickle.PickleError``. """ @abstractmethod @@ -208,8 +250,13 @@ def touch( backend: t.Optional[str] = None, ) -> bool: """ - Update the key's expiry time using ttl. Return True if successful - or False if the key does not exist. + Update the key's expiry time using ttl. + + :param key: The key to update. + :param ttl: The new time-to-live. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: `True` if successful or `False` if the key does not exist. """ @abstractmethod @@ -218,6 +265,11 @@ def has_key( ) -> bool: """ Return True if the key is in the cache and has not expired. + + :param key: The key to check. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: `True` if key is found and not expired, else `False`. """ @abstractmethod @@ -229,7 +281,13 @@ def incr( backend: t.Optional[str] = None, ) -> int: """ - Increments the number stored at key by one. If the key does not exist, it is set to 0 + Increments the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to increment. + :param delta: The amount to increment by. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The new value. """ @abstractmethod @@ -241,7 +299,13 @@ def decr( backend: t.Optional[str] = None, ) -> int: """ - Decrements the number stored at key by one. If the key does not exist, it is set to 0 + Decrements the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to decrement. + :param delta: The amount to decrement by. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The new value. """ @@ -250,23 +314,26 @@ class ICacheServiceAsync(ABC): async def get_async( self, key: str, version: t.Optional[str] = None, backend: t.Optional[str] = None ) -> t.Any: - """Look up key in the cache and return the value for it. - :param key: the key to be looked up. - :param version: the version for the key - :param backend: a backend service name - :returns: The value if it exists and is readable, else ``None``. + """ + Look up key in the cache and return the value for it. + + :param key: The key to be looked up. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The value if it exists and is readable, else ``None``. """ @abstractmethod async def delete_async( self, key: str, version: t.Optional[str] = None, backend: t.Optional[str] = None ) -> bool: - """Delete `key` from the cache. - :param key: the key to delete. - :param version: the version for the key - :param backend: a backend service name - :returns: Whether the key existed and has been deleted. - :rtype: boolean + """ + Delete `key` from the cache. + + :param key: The key to delete. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: Whether the key existed and has been deleted. """ @abstractmethod @@ -278,19 +345,18 @@ async def set_async( version: t.Optional[str] = None, backend: t.Optional[str] = None, ) -> bool: - """Add a new key/value to the cache (overwrites value, if key already - exists in the cache). - :param key: the key to set - :param value: the value for the key - :param version: the version for the key - :param backend: a backend service name - :param ttl: the cache ttl for the key in seconds (if not - specified, it uses the default ttl). A ttl of - 0 indicates that the cache never expires. - :returns: ``True`` if key has been updated, ``False`` for backend - errors. Pickling errors, however, will raise a subclass of - ``pickle.PickleError``. - :rtype: boolean + """ + Add a new key/value to the cache (overwrites value, if key already exists in the cache). + + :param key: The key to set. + :param value: The value to be cached. + :param ttl: The cache time-to-live for the key in seconds. + If not specified, the default TTL is used. + A TTL of 0 indicates that the cache never expires. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: ``True`` if key has been updated, ``False`` for backend errors. + Pickling errors, however, will raise a subclass of ``pickle.PickleError``. """ @abstractmethod @@ -302,8 +368,13 @@ async def touch_async( backend: t.Optional[str] = None, ) -> bool: """ - Update the key's expiry time using ttl. Return True if successful - or False if the key does not exist. + Update the key's expiry time using ttl. + + :param key: The key to update. + :param ttl: The new time-to-live. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: `True` if successful or `False` if the key does not exist. """ @abstractmethod @@ -312,6 +383,11 @@ async def has_key_async( ) -> bool: """ Return True if the key is in the cache and has not expired. + + :param key: The key to check. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: `True` if key is found and not expired, else `False`. """ @abstractmethod @@ -323,7 +399,13 @@ async def incr_async( backend: t.Optional[str] = None, ) -> int: """ - Increments the number stored at key by one. If the key does not exist, it is set to 0 + Increments the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to increment. + :param delta: The amount to increment by. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The new value. """ @abstractmethod @@ -335,7 +417,13 @@ async def decr_async( backend: t.Optional[str] = None, ) -> int: """ - Decrements the number stored at key by one. If the key does not exist, it is set to 0 + Decrements the number stored at key by one. If the key does not exist, it is set to 0. + + :param key: The key to decrement. + :param delta: The amount to decrement by. + :param version: The version for the key. + :param backend: The name of the backend service to use. + :return: The new value. """ @@ -346,6 +434,7 @@ class ICacheService(ICacheServiceSync, ICacheServiceAsync, ABC): def get_backend(self, backend: t.Optional[str] = None) -> "BaseCacheBackend": """ Return a given Cache Backend configured by configuration backend name. - :param backend: Cache Backend configuration name. If not set, 'default' backend will be returned + + :param backend: Cache Backend configuration name. If not set, 'default' backend will be returned. :return: BaseCacheBackend """ diff --git a/ellar/cache/make_key_decorator.py b/ellar/cache/make_key_decorator.py index 1ca2d942..8c8d2b7c 100644 --- a/ellar/cache/make_key_decorator.py +++ b/ellar/cache/make_key_decorator.py @@ -8,6 +8,10 @@ class MakeKeyDecorator: + """ + A helper class to create decorators that handle cache key generation and validation. + """ + __slots__ = ("_func", "_validate", "_is_async") def __init__(self, func: t.Callable, validate: bool = False) -> None: @@ -55,11 +59,23 @@ def _wrap( @t.no_type_check def make_key_decorator(func: t.Callable) -> t.Callable[..., t.Awaitable]: + """ + Decorator to automatically generate a cache key for the backend method. + + :param func: The backend method to decorate. + :return: Wrapped function with key generation. + """ make_key = MakeKeyDecorator(func, validate=False) return make_key.get_decorator() @t.no_type_check def make_key_decorator_and_validate(func: t.Callable) -> t.Callable[..., t.Awaitable]: + """ + Decorator to generate a cache key and validate it for the backend method. + + :param func: The backend method to decorate. + :return: Wrapped function with key generation and validation. + """ make_key = MakeKeyDecorator(func, validate=True) return make_key.get_decorator() diff --git a/ellar/cache/module.py b/ellar/cache/module.py index 4f377777..4286cbd9 100644 --- a/ellar/cache/module.py +++ b/ellar/cache/module.py @@ -13,6 +13,10 @@ @Module(exports=[ICacheService]) class CacheModule(ModuleBase, IModuleSetup): + """ + Cache Module responsible for setting up cache services. + """ + @classmethod def _create_dynamic_module(cls, schema: CacheModuleSchemaSetup) -> DynamicModule: cache_service = CacheService( diff --git a/ellar/cache/schema.py b/ellar/cache/schema.py index cc03793d..a94bccbb 100644 --- a/ellar/cache/schema.py +++ b/ellar/cache/schema.py @@ -6,6 +6,10 @@ @as_pydantic_validator("__validate_input__", schema={"type": "object"}) class TBaseCacheBackend: + """ + Validator to ensure input is an instance of BaseCacheBackend. + """ + @classmethod def __validate_input__( cls: t.Type["TBaseCacheBackend"], __input: t.Any, _: t.Any @@ -17,10 +21,17 @@ def __validate_input__( class CacheModuleSchemaSetup(BaseModel): + """ + Schema for Cache Module Setup. + """ + CACHES: t.Dict[str, TBaseCacheBackend] = {} @field_validator("CACHES", mode="before") def pre_cache_validate(cls, value: t.Dict) -> t.Any: + """ + Validates the CACHES configuration to ensure a 'default' backend is present. + """ if value and not value.get("default"): raise ValueError("CACHES configuration must have a 'default' key") return value diff --git a/ellar/cache/service.py b/ellar/cache/service.py index 358d4b1e..1209902f 100644 --- a/ellar/cache/service.py +++ b/ellar/cache/service.py @@ -77,7 +77,7 @@ def has_key( @injectable class CacheService(_CacheServiceSync, ICacheService): """ - A Cache Backend Service that wraps Ellar cache backends + A Cache Backend Service that wraps Ellar cache backends. """ def __init__( diff --git a/ellar/common/constants.py b/ellar/common/constants.py index 906576b8..ab278422 100644 --- a/ellar/common/constants.py +++ b/ellar/common/constants.py @@ -61,6 +61,10 @@ class MODULE_METADATA(metaclass=AnnotationToValue): + """ + Module Metadata Constants + """ + NAME: str CONTROLLERS: str BASE_DIRECTORY: str @@ -74,6 +78,10 @@ class MODULE_METADATA(metaclass=AnnotationToValue): class CONTROLLER_METADATA(metaclass=AnnotationToValue): + """ + Controller Metadata Constants + """ + PATH: str NAME: str INCLUDE_IN_SCHEMA: str @@ -91,6 +99,10 @@ class CONTROLLER_METADATA(metaclass=AnnotationToValue): class LOG_LEVELS(Enum): + """ + Ellar Log Levels Variables + """ + critical = logging.CRITICAL error = logging.ERROR warning = logging.WARNING @@ -100,6 +112,10 @@ class LOG_LEVELS(Enum): class NOT_SET_TYPE: + """ + Ellar Not Set Type + """ + def __repr__(self) -> str: # pragma: no cover return f"{__name__}.{self.__class__.__name__}" diff --git a/ellar/common/converters.py b/ellar/common/converters.py index 7b3d1fad..d3f2b264 100644 --- a/ellar/common/converters.py +++ b/ellar/common/converters.py @@ -24,6 +24,10 @@ def _get_origin(outer_type_: t.Any) -> object: class TypeDefinitionConverter(ABC): + """ + TypeDefinitionConverter is a base class for converting type definitions. + """ + def __init__(self, outer_type_: t.Any) -> None: self.type_origin = _get_origin(outer_type_) self.sub_fields = self.get_sub_fields(get_args(outer_type_)) diff --git a/ellar/common/decorators/base.py b/ellar/common/decorators/base.py index 9ff6c06a..9da8794d 100644 --- a/ellar/common/decorators/base.py +++ b/ellar/common/decorators/base.py @@ -9,6 +9,17 @@ def set_metadata( meta_key: t.Any, meta_value: t.Optional[t.Any] = NOT_SET, ) -> t.Callable: + """ + Sets metadata for a given target. + + ### Example + + ```python + @set_metadata("role", "admin") + def some_function(): + pass + ``` + """ if meta_value is NOT_SET: return partial(set_metadata, meta_key) diff --git a/ellar/common/decorators/controller.py b/ellar/common/decorators/controller.py index 657b94fa..dd30aefc 100644 --- a/ellar/common/decorators/controller.py +++ b/ellar/common/decorators/controller.py @@ -30,12 +30,22 @@ def Controller( ========= CLASS DECORATOR ============== Controller Class Decorator + :param prefix: Route Prefix default=[ControllerName] :param name: route name prefix for url reversing, eg name:route_name default='' :param include_in_schema: include controller in OPENAPI schema :param scope: Controller Instance Lifetime scope :param middleware: Controller Middlewares - :return: t.Type[ControllerBase] + + ### Example + + ```python + @Controller("/users") + class UserController(ControllerBase): + @get("/") + def index(self): + return {"message": "Hello World"} + ``` """ _prefix: t.Optional[t.Any] = prefix if prefix is not None else NOT_SET if prefix and isinstance(prefix, type): diff --git a/ellar/common/decorators/exception.py b/ellar/common/decorators/exception.py index 4a2de287..3b4c2efb 100644 --- a/ellar/common/decorators/exception.py +++ b/ellar/common/decorators/exception.py @@ -29,7 +29,19 @@ def exception_handler( Defines Exception Handler at Module Level :param exc_or_status_code: Exception Class or Status Code :param app: Indicates Exception Handler will be global to the application - :return: Function + + ### Example + + ```python + from ellar.common import Module, IHostContext + from ellar.common.responses import JSONResponse + + @Module() + class AModule: + @exception_handler(404) + def not_found(self, context: IHostContext, exc: Exception) -> Response: + return JSONResponse({"message": "Not Found"}, status_code=404) + ``` """ def decorator(func: t.Union[t.Callable, t.Type]) -> t.Callable: diff --git a/ellar/common/decorators/extra_args.py b/ellar/common/decorators/extra_args.py index a2cd51b4..8d255df6 100644 --- a/ellar/common/decorators/extra_args.py +++ b/ellar/common/decorators/extra_args.py @@ -12,9 +12,20 @@ def extra_args(*args: "ExtraEndpointArg") -> t.Callable: """ =========FUNCTION DECORATOR ============== - Programmatically adds extra route function parameter. - see https://github.com/eadwinCode/ellar/blob/main/tests/test_routing/test_extra_args.py for usages + **Programmatically** adds extra route function parameter. + :param args: Collection ExtraEndpointArg - :return: + + ### Example + + ```python + from ellar.common import extra_args, Query + from ellar.common.params import ExtraEndpointArg + + @get("/") + @extra_args(ExtraEndpointArg(name="query1", annotation=str, default_value=Query())) + def index(q: str): + return {"q": q} + ``` """ return set_meta(EXTRA_ROUTE_ARGS_KEY, list(args)) diff --git a/ellar/common/decorators/file.py b/ellar/common/decorators/file.py index 37486b4b..69e6298e 100644 --- a/ellar/common/decorators/file.py +++ b/ellar/common/decorators/file.py @@ -19,6 +19,7 @@ def file(media_type: t.Optional[str] = NOT_SET, streaming: bool = False) -> t.Ca :param media_type: MIME Type. :param streaming: Defaults ResponseModel to use. False=FileResponseModel, True=StreamingResponseModel. + IF STREAMING == FALSE: Decorated Function is expected to return an object of dict with values keys: { @@ -29,10 +30,19 @@ def file(media_type: t.Optional[str] = NOT_SET, streaming: bool = False) -> t.Ca content_disposition_type: `attachment` | `inline` status_code: 200 } + IF STREAMING == TRUE Decorated Function is expected to return: typing.Iterator[Content] OR typing.AsyncIterable[Content] - :return: typing.Callable + + ### Example + + ```python + @get("/file") + @file() + def get_file(): + return {"path": "path/to/file.txt", "filename": "file.txt"} + ``` """ if media_type is not NOT_SET: assert isinstance(media_type, str), "File decorator must invoked eg. @file()" diff --git a/ellar/common/decorators/guards.py b/ellar/common/decorators/guards.py index bd350b83..02600c9f 100644 --- a/ellar/common/decorators/guards.py +++ b/ellar/common/decorators/guards.py @@ -30,6 +30,20 @@ def UseGuards( :param _guards: A single guard instance or class, or a list of guard instances or classes. - :return: + + ### Example + + ```python + from ellar.common import UseGuards, GuardCanActivate + from ellar.core import ExecutionContext + + class MyGuard(GuardCanActivate): + async def can_activate(self, context: ExecutionContext) -> bool: + return True + + @UseGuards(MyGuard) + def index(): + return {"message": "Hello World"} + ``` """ return set_meta(GUARDS_KEY, list(_guards)) diff --git a/ellar/common/decorators/html.py b/ellar/common/decorators/html.py index d0496903..1235decf 100644 --- a/ellar/common/decorators/html.py +++ b/ellar/common/decorators/html.py @@ -38,7 +38,14 @@ def render(template_name: t.Optional[str] = NOT_SET) -> t.Callable: :param template_name: template name. - :return: + ### Example + + ```python + @get("/html") + @render("index.html") + def get_html(): + return {"message": "Hello World"} + ``` """ if template_name is not NOT_SET: assert isinstance(template_name, str), ( diff --git a/ellar/common/decorators/interceptor.py b/ellar/common/decorators/interceptor.py index b0351c5b..47ee80a1 100644 --- a/ellar/common/decorators/interceptor.py +++ b/ellar/common/decorators/interceptor.py @@ -28,6 +28,22 @@ def UseInterceptors( using `app.use_global_interceptors()`. :param args: A single EllarInterceptor instance or class, or a list of EllarInterceptor instances or classes. - :return: + + ### Example + + ```python + from ellar.common import UseInterceptors, EllarInterceptor, IExecutionContext + + class LoggingInterceptor(EllarInterceptor): + async def intercept(self, context: IExecutionContext, next_interceptor): + print("Before...") + result = await next_interceptor() + print("After...") + return result + + @UseInterceptors(LoggingInterceptor) + def index(): + return {"message": "Hello World"} + ``` """ return set_meta(ROUTE_INTERCEPTORS, list(args)) diff --git a/ellar/common/decorators/middleware.py b/ellar/common/decorators/middleware.py index acf7ec76..f517da4e 100644 --- a/ellar/common/decorators/middleware.py +++ b/ellar/common/decorators/middleware.py @@ -19,16 +19,18 @@ def middleware(app: bool = False) -> t.Callable: Defines middleware functions at @Module decorated class level - Usage: - - @middleware() - async def my_middleware(cls, context: IExecutionContext, call_next): - print("Called my_middleware") - request = context.switch_to_http_connection().get_request() - request.state.my_middleware = True - await call_next() - - :return: Function + ### Example + + ```python + @Module() + class AModule: + @middleware() + async def my_middleware(cls, context: IExecutionContext, call_next): + print("Called my_middleware") + request = context.switch_to_http_connection().get_request() + request.state.my_middleware = True + await call_next() + ``` """ def decorator(func: t.Callable) -> t.Callable: diff --git a/ellar/common/decorators/modules.py b/ellar/common/decorators/modules.py index 76c62a40..c8c200ef 100644 --- a/ellar/common/decorators/modules.py +++ b/ellar/common/decorators/modules.py @@ -89,7 +89,19 @@ def Module( :param commands: List of Command Decorated functions and EllarTyper - :return: t.TYPE[ModuleBase] + ### Example + + ```python + from ellar.common import Module, Controller + + @Controller() + class MyController: + pass + + @Module(controllers=[MyController]) + class MyModule: + pass + ``` """ base_directory = get_main_directory_by_stack(base_directory, stack_level=2) kwargs = AttributeDict( diff --git a/ellar/common/decorators/serializer.py b/ellar/common/decorators/serializer.py index 01ffa924..bb76d34f 100644 --- a/ellar/common/decorators/serializer.py +++ b/ellar/common/decorators/serializer.py @@ -22,13 +22,22 @@ def serializer_filter( ========= ROUTE FUNCTION DECORATOR ============== defines route function pydantic filters for data serialization - :param include: - :param exclude: - :param by_alias: - :param exclude_unset: - :param exclude_defaults: - :param exclude_none: - :return: + + :param include: Fields to include + :param exclude: Fields to exclude + :param by_alias: Whether to use alias + :param exclude_unset: Whether to exclude unset fields + :param exclude_defaults: Whether to exclude default fields + :param exclude_none: Whether to exclude none fields + + ### Example + + ```python + @get("/") + @serializer_filter(include={"id", "username"}) + def index(): + return {"id": 1, "username": "admin", "password": "password"} + ``` """ return set_meta( diff --git a/ellar/common/decorators/versioning.py b/ellar/common/decorators/versioning.py index 35f763b9..f4c45bdb 100644 --- a/ellar/common/decorators/versioning.py +++ b/ellar/common/decorators/versioning.py @@ -11,6 +11,14 @@ def Version(*_version: str) -> t.Callable: Defines route function version :param _version: allowed versions - :return: + + ### Example + + ```python + @get("/") + @Version("1.0", "1.1") + def index(): + return {"version": "1.0"} + ``` """ return set_meta(VERSIONING_KEY, {str(i) for i in _version}) diff --git a/ellar/common/responses/models/base.py b/ellar/common/responses/models/base.py index 29cf1d27..9337f0f6 100644 --- a/ellar/common/responses/models/base.py +++ b/ellar/common/responses/models/base.py @@ -33,7 +33,7 @@ def validate_object(self, obj: t.Any) -> t.Any: ) values, error = self.validate(obj, {}, loc=(self.alias,)) if error: - _errors = list(error) if isinstance(error, list) else [error] + _errors = list(error) if isinstance(error, list) else [error] # type: ignore[list-item] return None, _errors return values, [] diff --git a/ellar/common/shortcuts.py b/ellar/common/shortcuts.py index 5ab27d76..e9d0cc9f 100644 --- a/ellar/common/shortcuts.py +++ b/ellar/common/shortcuts.py @@ -1,4 +1,7 @@ def normalize_path(path: str) -> str: + """ + Normalizes a path by removing duplicate slashes. + """ while "//" in path: path = path.replace("//", "/") return path diff --git a/ellar/events/build.py b/ellar/events/build.py index 1925bd10..dcdd8fd7 100644 --- a/ellar/events/build.py +++ b/ellar/events/build.py @@ -12,24 +12,33 @@ def ensure_build_context(app_ready: bool = False) -> t.Callable: """ - Ensure function runs under injector_context or when injector_context when bootstrapping application - This is useful when running a function that modifies Application config but at the time execution, - build_context is not available is not ready. + Ensures a function runs within an active `injector_context` or defers execution until the application is bootstrapping. - example: + This decorator is useful for functions that need to modify the Application configuration + or interact with the injector at execution time, handling cases where the + build context might not yet be ready. + + If the `injector_context` is not available when the decorated function is called, + execution is deferred until the context is established. + + Example: + ```python from ellar.core import config @ensure_build_context def set_config_value(key, value): config[key] = value + # These calls will run immediately if context is ready, + # or be deferred until bootstrapping overlaps. set_config_value("MY_SETTINGS", 45) set_config_value("MY_SETTINGS_2", 100) + ``` - :param app_ready: Determine when a decorated function is called. - True value executes decorated function when App is ready in build context chain - False value executes decorated function when injector_context is ready - :return: + :param app_ready: Determines the required state for execution. + If ``True``, the function is executed only when the `App` instance is ready within the build context. + If ``False`` (default), the function is executed as soon as the `injector_context` is available. + :return: A decorator that wraps the target function. """ async def _on_context( diff --git a/ellar/openapi/openapi_v3.py b/ellar/openapi/openapi_v3.py index 800678f8..9ce7af40 100644 --- a/ellar/openapi/openapi_v3.py +++ b/ellar/openapi/openapi_v3.py @@ -318,7 +318,7 @@ class HTTPBase(SecurityBase): scheme: str -class HTTPBearer(HTTPBase): +class HTTPBearer(HTTPBase): # type: ignore[override] scheme: t.Literal["bearer"] = "bearer" bearerFormat: t.Optional[str] = None diff --git a/ellar/reflect/__init__.py b/ellar/reflect/__init__.py index d7df561d..4060e2c1 100644 --- a/ellar/reflect/__init__.py +++ b/ellar/reflect/__init__.py @@ -1,3 +1,8 @@ +""" +Ellar Reflect: A module for managing metadata on callables and types. +Provides tools to attach, retrieve, and manage metadata for dependency injection and other framework features. +""" + from ._reflect import reflect from .utils import ensure_target, fail_silently, transfer_metadata diff --git a/ellar/reflect/_reflect.py b/ellar/reflect/_reflect.py index 69f1d8f1..a8d447c3 100644 --- a/ellar/reflect/_reflect.py +++ b/ellar/reflect/_reflect.py @@ -10,6 +10,12 @@ def _try_hash(item: t.Any) -> bool: + """ + Try to hash an item. + + :param item: The item to try and hash. + :return: True if the item is hashable, False otherwise. + """ try: hash(item), weakref.ref(item) return True @@ -18,6 +24,10 @@ def _try_hash(item: t.Any) -> bool: class _Hashable: + """ + A wrapper class to make unhashable items hashable by using their ID and string representation. + """ + def __init__(self, item_id: int, item_repr: str) -> None: self.item_id = item_id self.item_repr = item_repr @@ -39,6 +49,13 @@ def __repr__(self) -> str: @classmethod def force_hash(cls, item: t.Any) -> t.Union[t.Any, "_Hashable"]: + """ + Force an item to be hashable. If it's already hashable, return it. + If not, return a _Hashable wrapper or retrieve an existing one. + + :param item: The item to hash. + :return: The item or its _Hashable wrapper. + """ if not _try_hash(item): hashable = fail_silently( lambda: reflect._un_hashable[hash((id(item), repr(item)))] @@ -54,6 +71,13 @@ def force_hash(cls, item: t.Any) -> t.Union[t.Any, "_Hashable"]: def _get_actual_target( target: t.Union[t.Type, t.Callable], ) -> t.Union[t.Type, t.Callable]: + """ + Get the actual target for metadata operations. + Resolves proxies and ensures the target is hashable. + + :param target: The target to resolve. + :return: The resolved, hashable target. + """ target = get_original_target(target) return t.cast( t.Union[t.Type, t.Callable], _Hashable.force_hash(ensure_target(target)) @@ -61,6 +85,11 @@ def _get_actual_target( class _Reflect: + """ + Metadata manager class for storage and retrieval of metadata associated with types and callables. + Use `reflect` instance for all operations. + """ + __slots__ = ("_meta_data",) _un_hashable: t.Dict[int, _Hashable] = {} @@ -74,9 +103,21 @@ def __init__(self) -> None: ) def add_type_update_callback(self, type_: t.Type, func: t.Callable) -> None: + """ + Register a callback to handle updates for a specific metadata type. + + :param type_: The type of the metadata value. + :param func: The call back function to handle the update. + """ self._data_type_update_callbacks[type_] = func def add_un_hashable_type(self, value: _Hashable) -> _Hashable: + """ + Store an unhashable item wrapper. + + :param value: The _Hashable wrapper. + :return: The stored _Hashable wrapper. + """ self._un_hashable[hash(value)] = value return value @@ -91,6 +132,14 @@ def define_metadata( metadata_value: t.Any, target: t.Union[t.Type, t.Callable], ) -> t.Any: + """ + Define metadata for a target. + + :param metadata_key: The key for the metadata. + :param metadata_value: The value of the metadata. + :param target: The target object to associate the metadata with. + :return: The value returned by type update callback or new value. + """ if target is None: raise Exception("`target` is not a valid type") # if ( @@ -114,6 +163,14 @@ def define_metadata( target_metadata[metadata_key] = metadata_value def metadata(self, metadata_key: str, metadata_value: t.Any) -> t.Any: + """ + Decorator to define metadata on a class or function. + + :param metadata_key: The key for the metadata. + :param metadata_value: The value of the metadata. + :return: A decorator function. + """ + def _wrapper(target: t.Union[t.Type, t.Callable]) -> t.Any: self.define_metadata(metadata_key, metadata_value, target) return target @@ -123,6 +180,13 @@ def _wrapper(target: t.Union[t.Type, t.Callable]) -> t.Any: def has_metadata( self, metadata_key: str, target: t.Union[t.Type, t.Callable] ) -> bool: + """ + Check if metadata key exists for a target. + + :param metadata_key: The key to check. + :param target: The target object. + :return: True if metadata key exists, False otherwise. + """ _target_actual = _get_actual_target(target) target_metadata = self._meta_data.get(_target_actual) or {} @@ -131,6 +195,13 @@ def has_metadata( def get_metadata( self, metadata_key: str, target: t.Union[t.Type, t.Callable] ) -> t.Optional[t.Any]: + """ + Retrieve metadata value for a target. + + :param metadata_key: The key to retrieve. + :param target: The target object. + :return: The metadata value or None if not found. + """ _target_actual = _get_actual_target(target) target_metadata = self._meta_data.get(_target_actual) or {} @@ -143,6 +214,14 @@ def get_metadata( def get_metadata_search_safe( self, metadata_key: str, target: t.Union[t.Type, t.Callable] ) -> t.Any: + """ + Retrieve metadata value safely. Raises KeyError if key is not found in the target's metadata. + This behaves like `dict[key]`. + + :param metadata_key: The key to retrieve. + :param target: The target object. + :return: The metadata value. + """ _target_actual = _get_actual_target(target) meta = self._meta_data[_target_actual] @@ -155,6 +234,14 @@ def get_metadata_search_safe( def get_metadata_or_raise_exception( self, metadata_key: str, target: t.Union[t.Type, t.Callable] ) -> t.Any: + """ + Retrieve metadata or raise an Exception if not found. + + :param metadata_key: The key to retrieve. + :param target: The target object. + :return: The metadata value. + :raises Exception: If metadata key is not found. + """ value = self.get_metadata(metadata_key=metadata_key, target=target) if value is not None: return value @@ -163,17 +250,34 @@ def get_metadata_or_raise_exception( def get_metadata_keys( self, target: t.Union[t.Type, t.Callable] ) -> t.KeysView[t.Any]: + """ + Get all metadata keys for a target. + + :param target: The target object. + :return: A view of the metadata keys. + """ _target_actual = _get_actual_target(target) target_metadata = self._meta_data.get(_target_actual) or {} return target_metadata.keys() def get_all_metadata(self, target: t.Union[t.Type, t.Callable]) -> t.Dict: + """ + Get all metadata for a target as a dictionary. + + :param target: The target object. + :return: A dictionary containing all metadata. + """ _target_actual = _get_actual_target(target) target_metadata = self._meta_data.get(_target_actual) or {} return type(target_metadata)(target_metadata) def delete_all_metadata(self, target: t.Union[t.Type, t.Callable]) -> None: + """ + Delete all metadata for a target. + + :param target: The target object. + """ _target = _get_actual_target(target) if _target in self._meta_data: self._meta_data.pop(_target) @@ -181,6 +285,13 @@ def delete_all_metadata(self, target: t.Union[t.Type, t.Callable]) -> None: def delete_metadata( self, metadata_key: str, target: t.Union[t.Type, t.Callable] ) -> t.Any: + """ + Delete a specific metadata key for a target. + + :param metadata_key: The key to delete. + :param target: The target object. + :return: The deleted value or None. + """ _target_actual = _get_actual_target(target) target_metadata = self._meta_data.get(_target_actual) or {} @@ -215,6 +326,10 @@ def _clone_meta_data( @asynccontextmanager async def async_context(self) -> t.AsyncGenerator[None, None]: + """ + Async context manager that isolates metadata changes within the context. + Metadata changes made inside the context are discarded after exit. + """ cached_meta_data = self._clone_meta_data() yield reflect._meta_data.clear() @@ -222,6 +337,10 @@ async def async_context(self) -> t.AsyncGenerator[None, None]: @contextmanager def context(self) -> t.Generator: + """ + Context manager that isolates metadata changes within the context. + Metadata changes made inside the context are discarded after exit. + """ cached_meta_data = self._clone_meta_data() yield reflect._meta_data.clear() @@ -229,6 +348,13 @@ def context(self) -> t.Generator: def _list_update(existing_value: t.Any, new_value: t.Any) -> t.Any: + """ + Update callback for list/tuple types. Concatenates the new value to the existing value. + + :param existing_value: The existing list or tuple. + :param new_value: The new list or tuple. + :return: The concatenated list or tuple. + """ if isinstance(existing_value, (list, tuple)) and isinstance( new_value, (list, tuple) ): @@ -237,6 +363,13 @@ def _list_update(existing_value: t.Any, new_value: t.Any) -> t.Any: def _set_update(existing_value: t.Any, new_value: t.Any) -> t.Any: + """ + Update callback for set types. Unions the new value with the existing value. + + :param existing_value: The existing set. + :param new_value: The new set. + :return: The union of the sets. + """ if isinstance(existing_value, set) and isinstance(new_value, set): existing_combined = list(existing_value) + list(new_value) return type(existing_value)(existing_combined) @@ -244,6 +377,13 @@ def _set_update(existing_value: t.Any, new_value: t.Any) -> t.Any: def _dict_update(existing_value: t.Any, new_value: t.Any) -> t.Any: + """ + Update callback for dict types. Updates the existing dictionary with new values. + + :param existing_value: The existing dictionary. + :param new_value: The new dictionary. + :return: The updated dictionary. + """ if isinstance( existing_value, (dict, WeakKeyDictionary, WeakValueDictionary) ) and isinstance(new_value, (dict, WeakKeyDictionary, WeakValueDictionary)): diff --git a/ellar/reflect/utils.py b/ellar/reflect/utils.py index 47afa436..ae6bd60e 100644 --- a/ellar/reflect/utils.py +++ b/ellar/reflect/utils.py @@ -7,6 +7,12 @@ def ensure_target(target: t.Union[t.Type, t.Callable]) -> t.Union[t.Type, t.Callable]: + """ + Ensure the target is a class or a function, unwrapping methods to their underlying functions. + + :param target: The target object (class, function, or method). + :return: The class or function. + """ res = target if inspect.ismethod(res): res = target.__func__ @@ -14,14 +20,32 @@ def ensure_target(target: t.Union[t.Type, t.Callable]) -> t.Union[t.Type, t.Call def is_decorated_with_partial(func_or_class: t.Any) -> bool: + """ + Check if the object is decorated with `functools.partial`. + + :param func_or_class: The object to check. + :return: True if decorated with partial, False otherwise. + """ return isinstance(func_or_class, functools.partial) def is_decorated_with_wraps(func_or_class: t.Any) -> bool: + """ + Check if the object is decorated with `functools.wraps`. + + :param func_or_class: The object to check. + :return: True if decorated with wraps, False otherwise. + """ return hasattr(func_or_class, "__wrapped__") def get_original_target(func_or_class: t.Any) -> t.Any: + """ + Unwrap the object to find the original target, getting past partials and wraps. + + :param func_or_class: The object to unwrap. + :return: The original underlying object. + """ while True: if is_decorated_with_partial(func_or_class): func_or_class = func_or_class.func @@ -34,6 +58,13 @@ def get_original_target(func_or_class: t.Any) -> t.Any: def transfer_metadata( old_target: t.Any, new_target: t.Any, clean_up: bool = False ) -> None: + """ + Transfer metadata from one target to another. + + :param old_target: The source target. + :param new_target: The destination target. + :param clean_up: If True, delete metadata from the old target after transfer. + """ from ._reflect import reflect meta = reflect.get_all_metadata(old_target) @@ -46,6 +77,14 @@ def transfer_metadata( @t.no_type_check def fail_silently(func: t.Callable, *args: t.Any, **kwargs: t.Any) -> t.Optional[t.Any]: + """ + Execute a function and return None if an exception occurs, logging the error blindly. + + :param func: The function to execute. + :param args: Positional arguments for the function. + :param kwargs: Keyword arguments for the function. + :return: The result of the function or None if an exception occurred. + """ try: return func(*args, **kwargs) except Exception as ex: # pragma: no cover From 327ad52a0a41257ad775ea90deb8a8451a2305c6 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 28 Dec 2025 18:08:14 +0100 Subject: [PATCH 2/2] Adding more code documentation --- ellar/common/responses/models/base.py | 2 +- ellar/openapi/openapi_v3.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ellar/common/responses/models/base.py b/ellar/common/responses/models/base.py index 9337f0f6..29cf1d27 100644 --- a/ellar/common/responses/models/base.py +++ b/ellar/common/responses/models/base.py @@ -33,7 +33,7 @@ def validate_object(self, obj: t.Any) -> t.Any: ) values, error = self.validate(obj, {}, loc=(self.alias,)) if error: - _errors = list(error) if isinstance(error, list) else [error] # type: ignore[list-item] + _errors = list(error) if isinstance(error, list) else [error] return None, _errors return values, [] diff --git a/ellar/openapi/openapi_v3.py b/ellar/openapi/openapi_v3.py index 9ce7af40..800678f8 100644 --- a/ellar/openapi/openapi_v3.py +++ b/ellar/openapi/openapi_v3.py @@ -318,7 +318,7 @@ class HTTPBase(SecurityBase): scheme: str -class HTTPBearer(HTTPBase): # type: ignore[override] +class HTTPBearer(HTTPBase): scheme: t.Literal["bearer"] = "bearer" bearerFormat: t.Optional[str] = None