Skip to content

Commit 55bdce7

Browse files
authored
Создан базовый функционал сервиса и настроен docker
1. Созданы модели: * Film * Genre * Person 2. Добавлен базовый функционал сервиса: * настроен конфиг * настроен сервисы для подключения к Redis и ES * создан сервис `FilmService` * реализован endpoint детальной страницы фильма (`film_details`) 3. Добавлен Dockerfile для приложения 4. Добавлен docker-compose.yml
1 parent 9cd0cef commit 55bdce7

16 files changed

+362
-11
lines changed

.dockerignore

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
.pytest_cache
2+
tests
3+
docker
4+
src_local
5+
6+
# Git
7+
.git
8+
.gitignore
9+
.gitattributes
10+
11+
12+
# CI
13+
.codeclimate.yml
14+
.travis.yml
15+
.taskcluster.yml
16+
17+
# Docker
18+
docker-compose.yml
19+
Dockerfile
20+
.docker
21+
.dockerignore
22+
23+
# Byte-compiled / optimized / DLL files
24+
**/__pycache__/
25+
**/*.py[cod]
26+
27+
# C extensions
28+
*.so
29+
30+
# Distribution / packaging
31+
.Python
32+
env/
33+
build/
34+
develop-eggs/
35+
dist/
36+
downloads/
37+
eggs/
38+
lib/
39+
lib64/
40+
parts/
41+
sdist/
42+
var/
43+
*.egg-info/
44+
.installed.cfg
45+
*.egg
46+
47+
# PyInstaller
48+
# Usually these files are written by a python script from a template
49+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
50+
*.manifest
51+
*.spec
52+
53+
# Installer logs
54+
pip-log.txt
55+
pip-delete-this-directory.txt
56+
57+
# Unit test / coverage reports
58+
htmlcov/
59+
.tox/
60+
.coverage
61+
.cache
62+
nosetests.xml
63+
coverage.xml
64+
65+
# Translations
66+
*.mo
67+
*.pot
68+
69+
# Django stuff:
70+
*.log
71+
72+
# Sphinx documentation
73+
docs/_build/
74+
75+
# PyBuilder
76+
target/
77+
78+
# Virtual environment
79+
.env
80+
.venv/
81+
venv/
82+
83+
# PyCharm
84+
.idea
85+
86+
# Python mode for VIM
87+
.ropeproject
88+
**/.ropeproject
89+
90+
# Vim swap files
91+
**/*.swp
92+
93+
# VS Code
94+
.vscode/

Dockerfile

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM python:3.10-alpine
2+
3+
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
4+
5+
ARG HOME_DIR=/app
6+
7+
WORKDIR $HOME_DIR
8+
9+
EXPOSE 8000
10+
11+
COPY requirements.txt requirements.txt
12+
13+
RUN apk update \
14+
&& apk add build-base \
15+
&& pip install --no-cache-dir --upgrade pip \
16+
&& pip install --no-cache-dir -r requirements.txt

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@
1414
- Проходим ревью
1515
- Вливаем в ветку main (через squash & merge)
1616
- Оповещаем всех остальных (чтобы они подтянули изменения из main и сразу порешали merge conflict)
17+
18+
## Запуск проекта
19+
1. Создать файл .env в корне проекта и заполнить его по шаблону .env.example
20+
2. Запустить базу данных, если она еще не запущена
21+
3. Выполнить `docker-compose up -d`. Эта команда поднимает следующие сервисы: само приложение, elasticsearch и redis

docker-compose.yml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
version: '3'
2+
3+
services:
4+
app:
5+
build: .
6+
command: uvicorn main:app --host 0.0.0.0 --port 8000
7+
environment:
8+
- REDIS_HOST=redis
9+
- ELASTIC_HOST=es
10+
ports:
11+
- "8000:8000"
12+
volumes:
13+
- "./:/app"
14+
depends_on:
15+
- es
16+
- redis
17+
es:
18+
image: docker.io/elastic/elasticsearch:7.7.0
19+
env_file:
20+
- .env
21+
environment:
22+
- discovery.type=single-node
23+
volumes:
24+
- esdata01:/usr/share/elasticsearch/data
25+
ports:
26+
- ${ELASTIC_PORT}:9200
27+
deploy:
28+
resources:
29+
limits:
30+
memory: ${ELASTIC_MEM_LIMIT}
31+
redis:
32+
image: redis:7-alpine
33+
env_file:
34+
- .env
35+
ports:
36+
- ${REDIS_PORT}:6379
37+
38+
volumes:
39+
esdata01:

main.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
3+
import aioredis
4+
import uvicorn as uvicorn
5+
from elasticsearch import AsyncElasticsearch
6+
from fastapi import FastAPI
7+
from fastapi.responses import ORJSONResponse
8+
9+
from src.api.v1 import films
10+
from src.core.config import settings
11+
from src.core.logger import LOGGING
12+
from src.db import elastic, redis
13+
14+
app = FastAPI(
15+
title=settings.PROJECT_NAME,
16+
docs_url='/api/openapi',
17+
openapi_url='/api/openapi.json',
18+
default_response_class=ORJSONResponse,
19+
)
20+
21+
22+
@app.on_event('startup')
23+
async def startup():
24+
redis.redis = await aioredis.from_url(f'redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}')
25+
elastic.es = AsyncElasticsearch(hosts=[f'http://{settings.ELASTIC_HOST}:{settings.ELASTIC_PORT}'])
26+
27+
28+
@app.on_event('shutdown')
29+
async def shutdown():
30+
await redis.redis.close()
31+
await elastic.es.close()
32+
33+
34+
app.include_router(films.router, prefix='/api/v1/films', tags=['films'])
35+
36+
if __name__ == '__main__':
37+
uvicorn.run(
38+
'main:app',
39+
host='0.0.0.0',
40+
port=8000,
41+
log_config=LOGGING,
42+
log_level=logging.DEBUG,
43+
)

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
aioredis==2.0.1
2-
elasticsearch==7.9.1
2+
elasticsearch[async]==7.9.1
33
fastapi==0.78.0
44
orjson==3.7.7
55
pydantic==1.9.1
@@ -11,3 +11,4 @@ flake8-broken-line==0.4.0
1111
flake8-quotes==3.3.1
1212
isort==5.10.1
1313
pre-commit==2.20.0
14+
loguru==0.6.0

src/main.py src/__init__.py

File renamed without changes.

src/api/v1/films.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from http import HTTPStatus
2+
from typing import List
3+
4+
from fastapi import APIRouter, Depends, HTTPException
5+
from pydantic import BaseModel
6+
7+
from src.models.film import Genre, Person
8+
from src.models.mixins import UUIDMixin
9+
from src.services.film import FilmService, get_film_service
10+
11+
router = APIRouter()
12+
13+
14+
class FilmAPI(UUIDMixin, BaseModel):
15+
title: str
16+
imdb_rating: float
17+
description: str
18+
genre: List[Genre]
19+
actors: List[Person]
20+
writers: List[Person]
21+
directors: List[Person]
22+
23+
24+
@router.get('/{film_id}', response_model=FilmAPI)
25+
async def film_details(film_id: str, film_service: FilmService = Depends(get_film_service)) -> FilmAPI:
26+
film = await film_service.get_by_id(film_id)
27+
if not film:
28+
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail='film not found')
29+
30+
return FilmAPI.parse_obj(film.dict(by_alias=True))

src/core/config.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import os
22
from logging import config as logging_config
3+
from pathlib import Path
34

45
from pydantic import BaseSettings
56

6-
from core.logger import LOGGING
7+
from src.core.logger import LOGGING
8+
9+
logging_config.dictConfig(LOGGING)
10+
11+
BASE_DIR = Path(__file__).resolve().parent
712

813

914
class Settings(BaseSettings):
10-
PROJECT_NAME: str
11-
REDIS_HOST: str
12-
REDIS_PORT: str
13-
ELASTIC_HOST: str
14-
ELASTIC_PORT: str
15-
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15+
PROJECT_NAME: str = 'movies'
16+
REDIS_HOST: str = '127.0.0.1'
17+
REDIS_PORT: str = '6379'
18+
ELASTIC_HOST: str = '127.0.0.1'
19+
ELASTIC_PORT: str = '9200'
1620

1721
class Config:
18-
env_file = '.env'
22+
env_file = os.path.join(BASE_DIR, '.env')
1923
env_file_encoding = 'utf-8'
2024

2125

2226
settings = Settings()
23-
24-
logging_config.dictConfig(LOGGING)

src/models/film.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import List
2+
3+
import orjson
4+
5+
from src.models.genre import Genre
6+
from src.models.mixins import UUIDMixin
7+
from src.models.person import Person
8+
from src.models.utils import orjson_dumps
9+
10+
11+
class Film(UUIDMixin):
12+
title: str
13+
imdb_rating: float
14+
description: str = ''
15+
genre: List[Genre] = []
16+
actors: List[Person] = []
17+
writers: List[Person] = []
18+
directors: List[Person] = []
19+
20+
class Config:
21+
json_loads = orjson.loads
22+
json_dumps = orjson_dumps

src/models/genre.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import orjson
2+
3+
from src.models.mixins import UUIDMixin
4+
from src.models.utils import orjson_dumps
5+
6+
7+
class Genre(UUIDMixin):
8+
name: str
9+
10+
class Config:
11+
json_loads = orjson.loads
12+
json_dumps = orjson_dumps

src/models/mixins.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class UUIDMixin(BaseModel):
5+
uuid: str = Field(alias='id')

src/models/person.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import orjson
2+
from pydantic import Field
3+
4+
from src.models.mixins import UUIDMixin
5+
from src.models.utils import orjson_dumps
6+
7+
8+
class Person(UUIDMixin):
9+
full_name: str = Field(alias='name')
10+
11+
class Config:
12+
json_loads = orjson.loads
13+
json_dumps = orjson_dumps

src/models/utils.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import orjson
2+
3+
4+
def orjson_dumps(v, *, default):
5+
return orjson.dumps(v, default=default).decode()

0 commit comments

Comments
 (0)