diff --git a/async_api/src/Redme.md b/async_api/src/Redme.md new file mode 100644 index 0000000..ecc5fed --- /dev/null +++ b/async_api/src/Redme.md @@ -0,0 +1 @@ + содержит исходный код приложения diff --git a/async_api/src/api/__init__.py b/async_api/src/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/api/v1/Redme.md b/async_api/src/api/v1/Redme.md new file mode 100644 index 0000000..7895c9e --- /dev/null +++ b/async_api/src/api/v1/Redme.md @@ -0,0 +1 @@ +содержит разные конфигурационные файлы. diff --git a/async_api/src/api/v1/films.py b/async_api/src/api/v1/films.py index af887f9..25ccdf1 100644 --- a/async_api/src/api/v1/films.py +++ b/async_api/src/api/v1/films.py @@ -1,27 +1,14 @@ from fastapi import APIRouter from pydantic import BaseModel -# Объект router, в котором регистрируем обработчики router = APIRouter() -# FastAPI в качестве моделей использует библиотеку pydantic -# https://pydantic-docs.helpmanual.io -# У неё есть встроенные механизмы валидации, сериализации и десериализации -# Также она основана на дата-классах - -# Модель ответа API class Film(BaseModel): id: str title: str -# С помощью декоратора регистрируем обработчик film_details -# На обработку запросов по адресу /some_id -# Позже подключим роутер к корневому роутеру -# И адрес запроса будет выглядеть так — /api/v1/film/some_id -# В сигнатуре функции указываем тип данных, получаемый из адреса запроса (film_id: str) -# И указываем тип возвращаемого объекта — Film @router.get("/{film_id}", response_model=Film) -async def film_details(film_id: str) -> Film: - return Film(id="some_id", title="some_title") +async def film_details(film_id: str = "0") -> Film: + return Film(id=film_id, title="some_title") diff --git a/async_api/src/core/Redme.md b/async_api/src/core/Redme.md new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/core/__init__.py b/async_api/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/core/config.py b/async_api/src/core/config.py index 278eb70..83b7a36 100644 --- a/async_api/src/core/config.py +++ b/async_api/src/core/config.py @@ -1,22 +1,18 @@ import os from logging import config as logging_config +from pathlib import Path from core.logger import LOGGING -# Применяем настройки логирования logging_config.dictConfig(LOGGING) -# Название проекта. Используется в Swagger-документации PROJECT_NAME = os.getenv("PROJECT_NAME", "movies") -# Настройки Redis REDIS_HOST = os.getenv("REDIS_HOST", "127.0.0.1") -REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) +REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) -# Настройки Elasticsearch ELASTIC_HOST = os.getenv("ELASTIC_HOST", "127.0.0.1") -ELASTIC_PORT = int(os.getenv("ELASTIC_PORT", 9200)) +ELASTIC_PORT = int(os.getenv("ELASTIC_PORT", "9200")) -# Корень проекта -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/async_api/src/core/logger.py b/async_api/src/core/logger.py index eaf738a..2220ccb 100644 --- a/async_api/src/core/logger.py +++ b/async_api/src/core/logger.py @@ -1,10 +1,6 @@ LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" LOG_DEFAULT_HANDLERS = ["console"] -# В логгере настраивается логгирование uvicorn-сервера. -# Про логирование в Python можно прочитать в документации -# https://docs.python.org/3/howto/logging.html -# https://docs.python.org/3/howto/logging-cookbook.html LOGGING = { "version": 1, diff --git a/async_api/src/db/Redme.md b/async_api/src/db/Redme.md new file mode 100644 index 0000000..29fe54a --- /dev/null +++ b/async_api/src/db/Redme.md @@ -0,0 +1 @@ +предоставляет объекты баз данных (Redis, Elasticsearch) и провайдеры для внедрения зависимостей. Redis будет использоваться для кеширования, чтобы не нагружать лишний раз Elasticsearch. diff --git a/async_api/src/db/__init__.py b/async_api/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/db/elastic.py b/async_api/src/db/elastic.py index 942c3c2..7c7d588 100644 --- a/async_api/src/db/elastic.py +++ b/async_api/src/db/elastic.py @@ -1,10 +1,17 @@ -from typing import Optional +# 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 diff --git a/async_api/src/db/redis.py b/async_api/src/db/redis.py index b4995ab..ac9aae1 100644 --- a/async_api/src/db/redis.py +++ b/async_api/src/db/redis.py @@ -1,10 +1,16 @@ -from typing import Optional +# 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 diff --git a/async_api/src/main.py b/async_api/src/main.py index 4c4aed2..a3344c4 100644 --- a/async_api/src/main.py +++ b/async_api/src/main.py @@ -1,10 +1,12 @@ -import logging +# nimport logging +import os import uvicorn from api.v1 import films from core import config -from core.logger import LOGGING + +# nfrom core.logger import LOGGING from db import elastic, redis from elasticsearch import AsyncElasticsearch from fastapi import FastAPI @@ -20,24 +22,22 @@ @app.on_event("startup") -async def 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(): +async def shutdown() -> None: await redis.redis.close() await elastic.es.close() -# Подключаем роутер к серверу, указав префикс /v1/films -# Теги указываем для удобства навигации по документации app.include_router(films.router, prefix="/api/v1/films", tags=["films"]) if __name__ == "__main__": uvicorn.run( "main:app", - host="0.0.0.0", + host=str(os.getenv("HOST")), port=8000, ) diff --git a/async_api/src/models/Redme.md b/async_api/src/models/Redme.md new file mode 100644 index 0000000..a411763 --- /dev/null +++ b/async_api/src/models/Redme.md @@ -0,0 +1 @@ +содержит классы, описывающие бизнес-сущности, например, фильмы, жанры, актёров. diff --git a/async_api/src/models/__init__.py b/async_api/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/models/film.py b/async_api/src/models/film.py index 810bf11..8351c53 100644 --- a/async_api/src/models/film.py +++ b/async_api/src/models/film.py @@ -2,7 +2,8 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel -from services.film import FilmService, get_film_service + +from src.services.film import FilmService, get_film_service router = APIRouter() @@ -12,20 +13,13 @@ class Film(BaseModel): title: str -# Внедряем FilmService с помощью Depends(get_film_service) @router.get("/{film_id}", response_model=Film) -async def film_details(film_id: str, film_service: FilmService = Depends(get_film_service)) -> 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: - # Если фильм не найден, отдаём 404 статус - # Желательно пользоваться уже определёнными HTTP-статусами, которые содержат enum - # Такой код будет более поддерживаемым raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="film not found") - # Перекладываем данные из models.Film в Film - # Обратите внимание, что у модели бизнес-логики есть поле description - # Которое отсутствует в модели ответа API. - # Если бы использовалась общая модель для бизнес-логики и формирования ответов API - # вы бы предоставляли клиентам данные, которые им не нужны - # и, возможно, данные, которые опасно возвращать return Film(id=film.id, title=film.title) diff --git a/async_api/src/services/Redme.md b/async_api/src/services/Redme.md new file mode 100644 index 0000000..a0f8e83 --- /dev/null +++ b/async_api/src/services/Redme.md @@ -0,0 +1 @@ +главное в сервисе. В этом модуле находится реализация всей бизнес-логики. Таким образом она отделена от транспорта. Благодаря такому разделению, вам будет легче добавлять новые типы транспортов в сервис. Например, легко добавить RPC протокол поверх AMQP или Websockets. diff --git a/async_api/src/services/__init__.py b/async_api/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_api/src/services/film.py b/async_api/src/services/film.py index 2c6ff19..0344c80 100644 --- a/async_api/src/services/film.py +++ b/async_api/src/services/film.py @@ -1,15 +1,19 @@ -from typing import Optional +# nfrom typing import Optional +import sys + +sys.path.append("async_api/src/models/") from functools import lru_cache -from db.elastic import get_elastic -from db.redis import get_redis +# nfrom db.elastic import get_elastic +# nfrom db.redis import get_redis from elasticsearch import AsyncElasticsearch, NotFoundError -from fastapi import Depends -from models.film import Film from redis.asyncio import Redis -FILM_CACHE_EXPIRE_IN_SECONDS = 60 * 5 # 5 минут +# nfrom fastapi import Depends +from src.models.film import Film + +FILM_CACHE_EXPIRE_IN_SECONDS = 60 * 5 class FilmService: @@ -17,17 +21,13 @@ def __init__(self, redis: Redis, elastic: AsyncElasticsearch): self.redis = redis self.elastic = elastic - # get_by_id возвращает объект фильма. Он опционален, так как фильм может отсутствовать в базе async def get_by_id(self, film_id: str) -> Film | None: # Пытаемся получить данные из кеша, потому что оно работает быстрее film = await self._film_from_cache(film_id) if not film: - # Если фильма нет в кеше, то ищем его в Elasticsearch film = await self._get_film_from_elastic(film_id) if not film: - # Если он отсутствует в Elasticsearch, значит, фильма вообще нет в базе return None - # Сохраняем фильм в кеш await self._put_film_to_cache(film) return film @@ -40,27 +40,19 @@ async def _get_film_from_elastic(self, film_id: str) -> Film | None: return Film(**doc["_source"]) async def _film_from_cache(self, film_id: str) -> Film | None: - # Пытаемся получить данные о фильме из кеша, используя команду get - # https://redis.io/commands/get/ data = await self.redis.get(film_id) if not data: return None - # pydantic предоставляет удобное API для создания объекта моделей из json - film = Film.parse_raw(data) - return film + return Film.parse_raw(data) - async def _put_film_to_cache(self, film: Film): - # Сохраняем данные о фильме, используя команду set - # Выставляем время жизни кеша — 5 минут - # https://redis.io/commands/set/ - # pydantic позволяет сериализовать модель в json + 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 = Depends(get_redis), - elastic: AsyncElasticsearch = Depends(get_elastic), + redis: Redis, + elastic: AsyncElasticsearch, ) -> FilmService: return FilmService(redis, elastic)