Skip to content

Commit

Permalink
update files
Browse files Browse the repository at this point in the history
1 error
 ImportError: cannot import name 'FilmService' from partially initialized module 'src.services.film' (most likely due to a circular import) (/Users/ershov/Desktop/practicum-async-api/async_api/src/services/film.py)
  • Loading branch information
PavelErsh committed Mar 13, 2024
1 parent 4c13925 commit 5dd21fb
Show file tree
Hide file tree
Showing 19 changed files with 55 additions and 72 deletions.
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 @@
содержит разные конфигурационные файлы.
17 changes: 2 additions & 15 deletions async_api/src/api/v1/films.py
Original file line number Diff line number Diff line change
@@ -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_prefix>/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")
Empty file added async_api/src/core/Redme.md
Empty file.
Empty file added async_api/src/core/__init__.py
Empty file.
12 changes: 4 additions & 8 deletions async_api/src/core/config.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 0 additions & 4 deletions async_api/src/core/logger.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
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.
11 changes: 9 additions & 2 deletions async_api/src/db/elastic.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions async_api/src/db/redis.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 7 additions & 7 deletions async_api/src/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
)
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.
18 changes: 6 additions & 12 deletions async_api/src/models/film.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
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.
36 changes: 14 additions & 22 deletions async_api/src/services/film.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
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:
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
Expand All @@ -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)

0 comments on commit 5dd21fb

Please sign in to comment.