Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8 basic structure #12

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
813 changes: 811 additions & 2 deletions async_api/poetry.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions async_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ name = "async_api"

[tool.poetry.dependencies] # https://python-poetry.org/docs/dependency-specification/
python = "^3.12"
redis = "^5.0.3"
elasticsearch = {extras = ["async"], version = "^8.12.1"}
fastapi = "^0.110.0"
orjson = "^3.9.15"
pydantic = "^2.6.4"
uvicorn = "^0.28.0"
uvloop = "^0.19.0"

[tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/
black = "^24.2.0"
Expand Down
1 change: 1 addition & 0 deletions async_api/src/Redme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
содержит исходный код приложения
Empty file added async_api/src/api/__init__.py
Empty file.
1 change: 1 addition & 0 deletions async_api/src/api/v1/Redme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
содержит разные конфигурационные файлы.
Empty file.
14 changes: 14 additions & 0 deletions async_api/src/api/v1/films.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from fastapi import APIRouter
from pydantic import BaseModel

router = APIRouter()


class Film(BaseModel):
id: str
title: str


@router.get("/{film_id}", response_model=Film)
async def film_details(film_id: str = "0") -> Film:
return Film(id=film_id, title="some_title")
Empty file added async_api/src/core/Redme.md
Empty file.
Empty file added async_api/src/core/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions async_api/src/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os

from logging import config as logging_config
from pathlib import Path

from core.logger import LOGGING

logging_config.dictConfig(LOGGING)

PROJECT_NAME = os.getenv("PROJECT_NAME", "movies")

REDIS_HOST = os.getenv("REDIS_HOST", "127.0.0.1")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))

ELASTIC_HOST = os.getenv("ELASTIC_HOST", "127.0.0.1")
ELASTIC_PORT = int(os.getenv("ELASTIC_PORT", "9200"))

BASE_DIR = Path(__file__).resolve().parent.parent
58 changes: 58 additions & 0 deletions async_api/src/core/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
LOG_DEFAULT_HANDLERS = ["console"]


LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": LOG_FORMAT,
},
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"": {
"handlers": LOG_DEFAULT_HANDLERS,
"level": "INFO",
},
"uvicorn.error": {
"level": "INFO",
},
"uvicorn.access": {
"handlers": ["access"],
"level": "INFO",
"propagate": False,
},
},
"root": {
"level": "INFO",
"formatter": "verbose",
"handlers": LOG_DEFAULT_HANDLERS,
},
}
1 change: 1 addition & 0 deletions async_api/src/db/Redme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
предоставляет объекты баз данных (Redis, Elasticsearch) и провайдеры для внедрения зависимостей. Redis будет использоваться для кеширования, чтобы не нагружать лишний раз Elasticsearch.
Empty file added async_api/src/db/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions async_api/src/db/elastic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# nfrom typing import Optional

from elasticsearch import AsyncElasticsearch

es: AsyncElasticsearch | None = None


class GetElasticError(Exception):
def __init__(self, message: str = "Ellastic not found"):
self.message = message


async def get_elastic() -> AsyncElasticsearch:
es: AsyncElasticsearch | None = None
if es is None:
raise GetElasticError
return es
16 changes: 16 additions & 0 deletions async_api/src/db/redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# nfrom typing import Optional

from redis.asyncio import Redis

redis: Redis | None = None


class GetRedisError(Exception):
def __init__(self, message: str = "Redis not found"):
self.message = message


async def get_redis() -> Redis:
if redis is None:
raise GetRedisError
return redis
43 changes: 43 additions & 0 deletions async_api/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# nimport logging
import os

import uvicorn

from api.v1 import films
from core import config

# nfrom core.logger import LOGGING
from db import elastic, redis
from elasticsearch import AsyncElasticsearch
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from redis.asyncio import Redis

app = FastAPI(
title=config.PROJECT_NAME,
docs_url="/api/openapi",
openapi_url="/api/openapi.json",
default_response_class=ORJSONResponse,
)


@app.on_event("startup")
async def startup() -> None:
redis.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT)
elastic.es = AsyncElasticsearch(hosts=[f"{config.ELASTIC_HOST}:{config.ELASTIC_PORT}"])


@app.on_event("shutdown")
async def shutdown() -> None:
await redis.redis.close()
await elastic.es.close()


app.include_router(films.router, prefix="/api/v1/films", tags=["films"])

if __name__ == "__main__":
uvicorn.run(
"main:app",
host=str(os.getenv("HOST")),
port=8000,
)
1 change: 1 addition & 0 deletions async_api/src/models/Redme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
содержит классы, описывающие бизнес-сущности, например, фильмы, жанры, актёров.
Empty file.
25 changes: 25 additions & 0 deletions async_api/src/models/film.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from http import HTTPStatus

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

from src.services.film import FilmService, get_film_service

router = APIRouter()


class Film(BaseModel):
id: str
title: str


@router.get("/{film_id}", response_model=Film)
async def film_details(film_id: str, film_service: FilmService) -> Film:
if film_service is None:
film_service = Depends(get_film_service)

film = await film_service.get_by_id(film_id)
if not film:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="film not found")

return Film(id=film.id, title=film.title)
1 change: 1 addition & 0 deletions async_api/src/services/Redme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
главное в сервисе. В этом модуле находится реализация всей бизнес-логики. Таким образом она отделена от транспорта. Благодаря такому разделению, вам будет легче добавлять новые типы транспортов в сервис. Например, легко добавить RPC протокол поверх AMQP или Websockets.
Empty file.
58 changes: 58 additions & 0 deletions async_api/src/services/film.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# nfrom typing import Optional
import sys

sys.path.append("async_api/src/models/")

from functools import lru_cache

# nfrom db.elastic import get_elastic
# nfrom db.redis import get_redis
from elasticsearch import AsyncElasticsearch, NotFoundError
from redis.asyncio import Redis

# nfrom fastapi import Depends
from src.models.film import Film

FILM_CACHE_EXPIRE_IN_SECONDS = 60 * 5


class FilmService:
def __init__(self, redis: Redis, elastic: AsyncElasticsearch):
self.redis = redis
self.elastic = elastic

async def get_by_id(self, film_id: str) -> Film | None:
# Пытаемся получить данные из кеша, потому что оно работает быстрее
film = await self._film_from_cache(film_id)
if not film:
film = await self._get_film_from_elastic(film_id)
if not film:
return None
await self._put_film_to_cache(film)

return film

async def _get_film_from_elastic(self, film_id: str) -> Film | None:
try:
doc = await self.elastic.get(index="movies", id=film_id)
except NotFoundError:
return None
return Film(**doc["_source"])

async def _film_from_cache(self, film_id: str) -> Film | None:
data = await self.redis.get(film_id)
if not data:
return None

return Film.parse_raw(data)

async def _put_film_to_cache(self, film: Film) -> None:
await self.redis.set(film.id, film.json(), FILM_CACHE_EXPIRE_IN_SECONDS)


@lru_cache
def get_film_service(
redis: Redis,
elastic: AsyncElasticsearch,
) -> FilmService:
return FilmService(redis, elastic)
Loading