From fdd20d1ccc4ea2873a2402888f8c3c55eacce968 Mon Sep 17 00:00:00 2001 From: ZhymabekRoman Date: Fri, 15 Nov 2024 17:53:09 +0500 Subject: [PATCH] feat(api): integrate dependency injection and streamline configuration --- .../src/freedium_library/api/__init__.py | 2 +- .../src/freedium_library/api/app.py | 28 +++++++++++++++ .../src/freedium_library/api/config.py | 10 ++++-- .../src/freedium_library/api/container.py | 7 ++++ .../freedium_library/api/error/__init__.py | 5 +++ .../freedium_library/api/handlers/__init__.py | 3 ++ .../freedium_library/api/handlers/render.py | 12 +++++++ .../freedium_library/api/handlers/utils.py | 10 ++++++ .../src/freedium_library/api/lifespan.py | 14 ++++++++ .../api/middlewares/__init__.py | 5 +++ .../src/freedium_library/api/settings.py | 29 +++++++++++++++ .../services/medium/medium.py | 3 +- .../utils/http/client/__init__.py | 4 +++ .../utils/http/{ => client}/client.py | 35 +++---------------- .../utils/http/{ => client}/client_test.py | 0 .../utils/http/client/config.py | 28 +++++++++++++++ .../freedium_library/utils/meta/pydantic.py | 11 +++--- 17 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 freedium-library/src/freedium_library/api/error/__init__.py create mode 100644 freedium-library/src/freedium_library/api/handlers/__init__.py create mode 100644 freedium-library/src/freedium_library/api/handlers/render.py create mode 100644 freedium-library/src/freedium_library/api/handlers/utils.py create mode 100644 freedium-library/src/freedium_library/api/lifespan.py create mode 100644 freedium-library/src/freedium_library/api/middlewares/__init__.py create mode 100644 freedium-library/src/freedium_library/api/settings.py create mode 100644 freedium-library/src/freedium_library/utils/http/client/__init__.py rename freedium-library/src/freedium_library/utils/http/{ => client}/client.py (86%) rename freedium-library/src/freedium_library/utils/http/{ => client}/client_test.py (100%) create mode 100644 freedium-library/src/freedium_library/utils/http/client/config.py diff --git a/freedium-library/src/freedium_library/api/__init__.py b/freedium-library/src/freedium_library/api/__init__.py index f1e08cc..3c06bd1 100644 --- a/freedium-library/src/freedium_library/api/__init__.py +++ b/freedium-library/src/freedium_library/api/__init__.py @@ -1,4 +1,4 @@ -from freedium_library import __VERSION__ +from freedium_library.__init__ import __VERSION__ __NAME__ = "Freedium Library API" diff --git a/freedium-library/src/freedium_library/api/app.py b/freedium-library/src/freedium_library/api/app.py index e69de29..8c207da 100644 --- a/freedium-library/src/freedium_library/api/app.py +++ b/freedium-library/src/freedium_library/api/app.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI + +from freedium_library.api.container import APIContainer +from freedium_library.api.error import register_error_handler +from freedium_library.api.handlers import register_router +from freedium_library.api.lifespan import lifespan +from freedium_library.api.middlewares import register_middlewares +from freedium_library.api.settings import ApplicationSettings + +container = APIContainer() + + +def create_application() -> FastAPI: + settings = ApplicationSettings() + + if container.config.DISABLE_DOCS: + settings.disable_docs() + + app = FastAPI(**settings.to_dict(), lifespan=lifespan) # type: ignore + + register_router(app, container.config.PREFIX_PATH) + register_error_handler(app) + register_middlewares(app) + + return app + + +app = create_application() diff --git a/freedium-library/src/freedium_library/api/config.py b/freedium-library/src/freedium_library/api/config.py index 01f8530..9fcb858 100644 --- a/freedium-library/src/freedium_library/api/config.py +++ b/freedium-library/src/freedium_library/api/config.py @@ -1,2 +1,8 @@ -class APIConfig: - DISABLE_EXTERNAL_DOCS: bool = True +from freedium_library.utils.meta.pydantic import BaseConfig, BaseSettingsConfigDict + + +class APIConfig(BaseConfig): + model_config = BaseSettingsConfigDict(env_prefix="API_") + + DISABLE_DOCS: bool = True + PREFIX_PATH: str = "/api" diff --git a/freedium-library/src/freedium_library/api/container.py b/freedium-library/src/freedium_library/api/container.py index e69de29..563df5f 100644 --- a/freedium-library/src/freedium_library/api/container.py +++ b/freedium-library/src/freedium_library/api/container.py @@ -0,0 +1,7 @@ +from dependency_injector import containers, providers + +from freedium_library.api.config import APIConfig + + +class APIContainer(containers.DeclarativeContainer): + config: APIConfig = providers.Singleton(APIConfig) # type: ignore diff --git a/freedium-library/src/freedium_library/api/error/__init__.py b/freedium-library/src/freedium_library/api/error/__init__.py new file mode 100644 index 0000000..7b5f5bc --- /dev/null +++ b/freedium-library/src/freedium_library/api/error/__init__.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI + + +def register_error_handler(app: FastAPI) -> None: + pass diff --git a/freedium-library/src/freedium_library/api/handlers/__init__.py b/freedium-library/src/freedium_library/api/handlers/__init__.py new file mode 100644 index 0000000..78cfdef --- /dev/null +++ b/freedium-library/src/freedium_library/api/handlers/__init__.py @@ -0,0 +1,3 @@ +from .utils import register_router + +__all__ = ["register_router"] diff --git a/freedium-library/src/freedium_library/api/handlers/render.py b/freedium-library/src/freedium_library/api/handlers/render.py new file mode 100644 index 0000000..c3833b2 --- /dev/null +++ b/freedium-library/src/freedium_library/api/handlers/render.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +handler_router = APIRouter() + + +@handler_router.get("/render") +async def render_page(): + return "render" + + +def register_render_router(router: APIRouter) -> None: + router.add_api_route(path="/render", endpoint=render_page, methods=["GET"]) diff --git a/freedium-library/src/freedium_library/api/handlers/utils.py b/freedium-library/src/freedium_library/api/handlers/utils.py new file mode 100644 index 0000000..1c06519 --- /dev/null +++ b/freedium-library/src/freedium_library/api/handlers/utils.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter, FastAPI + +from .render import register_render_router + + +def register_router(app: FastAPI, router_prefix: str) -> None: + router = APIRouter(prefix=router_prefix) + register_render_router(router) + + app.include_router(router) diff --git a/freedium-library/src/freedium_library/api/lifespan.py b/freedium-library/src/freedium_library/api/lifespan.py new file mode 100644 index 0000000..18a861f --- /dev/null +++ b/freedium-library/src/freedium_library/api/lifespan.py @@ -0,0 +1,14 @@ +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from freedium_library.api.container import APIContainer + +container = APIContainer() + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + app.state.container = container + yield diff --git a/freedium-library/src/freedium_library/api/middlewares/__init__.py b/freedium-library/src/freedium_library/api/middlewares/__init__.py new file mode 100644 index 0000000..f6a9c6c --- /dev/null +++ b/freedium-library/src/freedium_library/api/middlewares/__init__.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI + + +def register_middlewares(app: FastAPI) -> None: + pass diff --git a/freedium-library/src/freedium_library/api/settings.py b/freedium-library/src/freedium_library/api/settings.py new file mode 100644 index 0000000..6031b9c --- /dev/null +++ b/freedium-library/src/freedium_library/api/settings.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + +from freedium_library import __NAME__, __VERSION__ +from freedium_library.api.container import APIContainer + +container = APIContainer() + + +@dataclass +class ApplicationSettings: + title: str = __NAME__ + version: str = __VERSION__ + openapi_url: str | None = f"{container.config.PREFIX_PATH}/openapi.json" + docs_url: str | None = f"{container.config.PREFIX_PATH}/docs" + redoc_url: str | None = f"{container.config.PREFIX_PATH}/redoc" + + def disable_docs(self) -> None: + self.openapi_url = None + self.docs_url = None + self.redoc_url = None + + def to_dict(self) -> dict[str, str | None]: + return { + "title": self.title, + "version": self.version, + "openapi_url": self.openapi_url, + "docs_url": self.docs_url, + "redoc_url": self.redoc_url, + } diff --git a/freedium-library/src/freedium_library/services/medium/medium.py b/freedium-library/src/freedium_library/services/medium/medium.py index 39dbd7a..65065ea 100644 --- a/freedium-library/src/freedium_library/services/medium/medium.py +++ b/freedium-library/src/freedium_library/services/medium/medium.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from dependency_injector.wiring import Provide @@ -27,7 +27,6 @@ class MediumService(BaseService): self.request = request self.api_service = api_service self.path_validator = path_validator - _content: Optional[str] = None def _is_valid(self, path: str) -> bool: return self.path_validator.is_valid(path) diff --git a/freedium-library/src/freedium_library/utils/http/client/__init__.py b/freedium-library/src/freedium_library/utils/http/client/__init__.py new file mode 100644 index 0000000..65f38d1 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/__init__.py @@ -0,0 +1,4 @@ +from .client import Request +from .config import RequestConfig + +__all__ = ["Request", "RequestConfig"] diff --git a/freedium-library/src/freedium_library/utils/http/client.py b/freedium-library/src/freedium_library/utils/http/client/client.py similarity index 86% rename from freedium-library/src/freedium_library/utils/http/client.py rename to freedium-library/src/freedium_library/utils/http/client/client.py index 6265910..b78d094 100644 --- a/freedium-library/src/freedium_library/utils/http/client.py +++ b/freedium-library/src/freedium_library/utils/http/client/client.py @@ -1,6 +1,5 @@ import warnings -from dataclasses import dataclass -from typing import Any, Dict, Literal, Optional +from typing import Any, Dict, Optional from httpx import ( AsyncClient, @@ -11,39 +10,13 @@ from httpx import ( Timeout, ) - -@dataclass -class RequestProxyConfig: - type: Literal["http", "https", "socks5"] - host: str - port: int - username: Optional[str] = None - password: Optional[str] = None - - @property - def url(self) -> str: - type = self.type.replace( - "https", "http" - ) # See: https://www.python-httpx.org/advanced/proxies/ - - proxy_url = f"{type}://" - if self.username and self.password: - proxy_url += f"{self.username}:{self.password}@" - - proxy_url += f"{self.host}:{self.port}" - return proxy_url - - -@dataclass -class RequestConfig: - timeout: int = 6 - retries: int = 3 - proxy: Optional[RequestProxyConfig] = None - # backoff_factor: float = 0.1 # not possible. Default value: 0.5. https://github.com/encode/httpx/discussions/1895 +from .config import RequestConfig # https://github.com/encode/httpx/discussions/1748 class Request: + __slots__ = ("config", "_in_context_manager") + def __init__(self, config: Optional[RequestConfig] = None): self.config = config or RequestConfig() self._in_context_manager = False diff --git a/freedium-library/src/freedium_library/utils/http/client_test.py b/freedium-library/src/freedium_library/utils/http/client/client_test.py similarity index 100% rename from freedium-library/src/freedium_library/utils/http/client_test.py rename to freedium-library/src/freedium_library/utils/http/client/client_test.py diff --git a/freedium-library/src/freedium_library/utils/http/client/config.py b/freedium-library/src/freedium_library/utils/http/client/config.py new file mode 100644 index 0000000..bebb83a --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/config.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Literal, Optional + + +@dataclass +class RequestProxyConfig: + type: Literal["http", "https", "socks5"] + host: str + port: int + username: Optional[str] = None + password: Optional[str] = None + + @property + def url(self) -> str: + type = self.type.replace("https", "http") + proxy_url = f"{type}://" + if self.username and self.password: + proxy_url += f"{self.username}:{self.password}@" + proxy_url += f"{self.host}:{self.port}" + return proxy_url + + +@dataclass +class RequestConfig: + timeout: int = 6 + retries: int = 3 + proxy: Optional[RequestProxyConfig] = None + # backoff_factor: float = 0.1 # not possible. Default value: 0.5. https://github.com/encode/httpx/discussions/1895 diff --git a/freedium-library/src/freedium_library/utils/meta/pydantic.py b/freedium-library/src/freedium_library/utils/meta/pydantic.py index c909f77..50b926e 100644 --- a/freedium-library/src/freedium_library/utils/meta/pydantic.py +++ b/freedium-library/src/freedium_library/utils/meta/pydantic.py @@ -1,16 +1,17 @@ from functools import partial from typing import cast -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic_settings import BaseSettings as _BaseSettings +from pydantic_settings import SettingsConfigDict as _SettingsConfigDict -class BaseConfig(BaseSettings): ... +class BaseConfig(_BaseSettings): ... -BaseSettingsConfigDict: SettingsConfigDict = cast( - SettingsConfigDict, +BaseSettingsConfigDict: _SettingsConfigDict = cast( + _SettingsConfigDict, partial( - SettingsConfigDict, + _SettingsConfigDict, env_file=".env", extra="ignore", env_nested_delimiter="__",