feat(api): integrate dependency injection and streamline configuration

This commit is contained in:
ZhymabekRoman 2024-11-15 17:53:09 +05:00
parent bed7382657
commit fdd20d1ccc
17 changed files with 165 additions and 41 deletions

View file

@ -1,4 +1,4 @@
from freedium_library import __VERSION__
from freedium_library.__init__ import __VERSION__
__NAME__ = "Freedium Library API"

View file

@ -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()

View file

@ -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"

View file

@ -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

View file

@ -0,0 +1,5 @@
from fastapi import FastAPI
def register_error_handler(app: FastAPI) -> None:
pass

View file

@ -0,0 +1,3 @@
from .utils import register_router
__all__ = ["register_router"]

View file

@ -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"])

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,5 @@
from fastapi import FastAPI
def register_middlewares(app: FastAPI) -> None:
pass

View file

@ -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,
}

View file

@ -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)

View file

@ -0,0 +1,4 @@
from .client import Request
from .config import RequestConfig
__all__ = ["Request", "RequestConfig"]

View file

@ -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

View file

@ -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

View file

@ -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="__",