├── .gitignore ├── README.md ├── cast-service ├── Dockerfile ├── app │ ├── api │ │ ├── casts.py │ │ ├── db.py │ │ ├── db_manager.py │ │ └── models.py │ └── main.py └── requirements.txt ├── docker-compose.yml ├── movie-service ├── .vscode │ └── settings.json ├── Dockerfile ├── app │ ├── api │ │ ├── db.py │ │ ├── db_manager.py │ │ ├── models.py │ │ ├── movies.py │ │ └── service.py │ └── main.py └── requirements.txt └── nginx_config.conf /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | __pycache__ 3 | .vscode 4 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-microservice-fastapi 2 | Learn to build your own microservice using Python and FastAPI 3 | 4 | ## How to run?? 5 | - Make sure you have installed `docker` and `docker-compose` 6 | - Run `docker-compose up -d` 7 | - Head over to http://localhost:8080/api/v1/movies/docs for movie service docs 8 | and http://localhost:8080/api/v1/casts/docs for cast service docs 9 | -------------------------------------------------------------------------------- /cast-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | 7 | RUN apt-get update \ 8 | && apt-get install gcc -y \ 9 | && apt-get clean 10 | 11 | RUN pip install -r /app/requirements.txt \ 12 | && rm -rf /root/.cache/pip 13 | 14 | COPY . /app/ -------------------------------------------------------------------------------- /cast-service/app/api/casts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from typing import List 3 | 4 | from app.api.models import CastOut, CastIn, CastUpdate 5 | from app.api import db_manager 6 | 7 | casts = APIRouter() 8 | 9 | @casts.post('/', response_model=CastOut, status_code=201) 10 | async def create_cast(payload: CastIn): 11 | cast_id = await db_manager.add_cast(payload) 12 | 13 | response = { 14 | 'id': cast_id, 15 | **payload.dict() 16 | } 17 | 18 | return response 19 | 20 | @casts.get('/{id}/', response_model=CastOut) 21 | async def get_cast(id: int): 22 | cast = await db_manager.get_cast(id) 23 | if not cast: 24 | raise HTTPException(status_code=404, detail="Cast not found") 25 | return cast -------------------------------------------------------------------------------- /cast-service/app/api/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import (Column, Integer, MetaData, String, Table, 4 | create_engine, ARRAY) 5 | 6 | from databases import Database 7 | 8 | DATABASE_URI = os.getenv('DATABASE_URI') 9 | 10 | engine = create_engine(DATABASE_URI) 11 | metadata = MetaData() 12 | 13 | casts = Table( 14 | 'casts', 15 | metadata, 16 | Column('id', Integer, primary_key=True), 17 | Column('name', String(50)), 18 | Column('nationality', String(20)), 19 | ) 20 | 21 | database = Database(DATABASE_URI) -------------------------------------------------------------------------------- /cast-service/app/api/db_manager.py: -------------------------------------------------------------------------------- 1 | from app.api.models import CastIn, CastOut, CastUpdate 2 | from app.api.db import casts, database 3 | 4 | 5 | async def add_cast(payload: CastIn): 6 | query = casts.insert().values(**payload.dict()) 7 | 8 | return await database.execute(query=query) 9 | 10 | async def get_cast(id): 11 | query = casts.select(casts.c.id==id) 12 | return await database.fetch_one(query=query) -------------------------------------------------------------------------------- /cast-service/app/api/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | 4 | class CastIn(BaseModel): 5 | name: str 6 | nationality: Optional[str] = None 7 | 8 | 9 | class CastOut(CastIn): 10 | id: int 11 | 12 | 13 | class CastUpdate(CastIn): 14 | name: Optional[str] = None -------------------------------------------------------------------------------- /cast-service/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from app.api.casts import casts 3 | from app.api.db import metadata, database, engine 4 | 5 | metadata.create_all(engine) 6 | 7 | app = FastAPI(openapi_url="/api/v1/casts/openapi.json", docs_url="/api/v1/casts/docs") 8 | 9 | @app.on_event("startup") 10 | async def startup(): 11 | await database.connect() 12 | 13 | @app.on_event("shutdown") 14 | async def shutdown(): 15 | await database.disconnect() 16 | 17 | app.include_router(casts, prefix='/api/v1/casts', tags=['casts']) -------------------------------------------------------------------------------- /cast-service/requirements.txt: -------------------------------------------------------------------------------- 1 | asyncpg==0.20.1 2 | databases[postgresql]==0.2.6 3 | fastapi==0.48.0 4 | SQLAlchemy==1.3.13 5 | uvicorn==0.11.2 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | movie_service: 5 | build: ./movie-service 6 | command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 7 | volumes: 8 | - ./movie-service/:/app/ 9 | ports: 10 | - 8001:8000 11 | environment: 12 | - DATABASE_URI=postgresql://movie_db_username:movie_db_password@movie_db/movie_db_dev 13 | - CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/ 14 | depends_on: 15 | - movie_db 16 | 17 | movie_db: 18 | image: postgres:12.1-alpine 19 | volumes: 20 | - postgres_data_movie:/var/lib/postgresql/data/ 21 | environment: 22 | - POSTGRES_USER=movie_db_username 23 | - POSTGRES_PASSWORD=movie_db_password 24 | - POSTGRES_DB=movie_db_dev 25 | 26 | cast_service: 27 | build: ./cast-service 28 | command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 29 | volumes: 30 | - ./cast-service/:/app/ 31 | ports: 32 | - 8002:8000 33 | environment: 34 | - DATABASE_URI=postgresql://cast_db_username:cast_db_password@cast_db/cast_db_dev 35 | depends_on: 36 | - cast_db 37 | 38 | cast_db: 39 | image: postgres:12.1-alpine 40 | volumes: 41 | - postgres_data_cast:/var/lib/postgresql/data/ 42 | environment: 43 | - POSTGRES_USER=cast_db_username 44 | - POSTGRES_PASSWORD=cast_db_password 45 | - POSTGRES_DB=cast_db_dev 46 | 47 | nginx: 48 | image: nginx:latest 49 | ports: 50 | - "8080:8080" 51 | volumes: 52 | - ./nginx_config.conf:/etc/nginx/conf.d/default.conf 53 | depends_on: 54 | - cast_service 55 | - movie_service 56 | 57 | volumes: 58 | postgres_data_movie: 59 | postgres_data_cast: -------------------------------------------------------------------------------- /movie-service/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "env/bin/python3.7" 3 | } -------------------------------------------------------------------------------- /movie-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | 7 | RUN apt-get update \ 8 | && apt-get install gcc -y \ 9 | && apt-get clean 10 | 11 | RUN pip install -r /app/requirements.txt \ 12 | && rm -rf /root/.cache/pip 13 | 14 | COPY . /app/ -------------------------------------------------------------------------------- /movie-service/app/api/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table, 4 | create_engine, ARRAY) 5 | 6 | from databases import Database 7 | 8 | DATABASE_URI = os.getenv('DATABASE_URI') 9 | 10 | engine = create_engine(DATABASE_URI) 11 | metadata = MetaData() 12 | 13 | movies = Table( 14 | 'movies', 15 | metadata, 16 | Column('id', Integer, primary_key=True), 17 | Column('name', String(50)), 18 | Column('plot', String(250)), 19 | Column('genres', ARRAY(String)), 20 | Column('casts_id', ARRAY(Integer)) 21 | ) 22 | 23 | database = Database(DATABASE_URI) -------------------------------------------------------------------------------- /movie-service/app/api/db_manager.py: -------------------------------------------------------------------------------- 1 | from app.api.models import MovieIn, MovieOut, MovieUpdate 2 | from app.api.db import movies, database 3 | 4 | 5 | async def add_movie(payload: MovieIn): 6 | query = movies.insert().values(**payload.dict()) 7 | 8 | return await database.execute(query=query) 9 | 10 | async def get_all_movies(): 11 | query = movies.select() 12 | return await database.fetch_all(query=query) 13 | 14 | async def get_movie(id): 15 | query = movies.select(movies.c.id==id) 16 | return await database.fetch_one(query=query) 17 | 18 | async def delete_movie(id: int): 19 | query = movies.delete().where(movies.c.id==id) 20 | return await database.execute(query=query) 21 | 22 | async def update_movie(id: int, payload: MovieIn): 23 | query = ( 24 | movies 25 | .update() 26 | .where(movies.c.id == id) 27 | .values(**payload.dict()) 28 | ) 29 | return await database.execute(query=query) -------------------------------------------------------------------------------- /movie-service/app/api/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | 4 | class MovieIn(BaseModel): 5 | name: str 6 | plot: str 7 | genres: List[str] 8 | casts_id: List[int] 9 | 10 | 11 | class MovieOut(MovieIn): 12 | id: int 13 | 14 | 15 | class MovieUpdate(MovieIn): 16 | name: Optional[str] = None 17 | plot: Optional[str] = None 18 | genres: Optional[List[str]] = None 19 | casts_id: Optional[List[int]] = None -------------------------------------------------------------------------------- /movie-service/app/api/movies.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, HTTPException 3 | 4 | from app.api.models import MovieOut, MovieIn, MovieUpdate 5 | from app.api import db_manager 6 | from app.api.service import is_cast_present 7 | 8 | movies = APIRouter() 9 | 10 | @movies.post('/', response_model=MovieOut, status_code=201) 11 | async def create_movie(payload: MovieIn): 12 | for cast_id in payload.casts_id: 13 | if not is_cast_present(cast_id): 14 | raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found") 15 | 16 | movie_id = await db_manager.add_movie(payload) 17 | response = { 18 | 'id': movie_id, 19 | **payload.dict() 20 | } 21 | 22 | return response 23 | 24 | @movies.get('/', response_model=List[MovieOut]) 25 | async def get_movies(): 26 | return await db_manager.get_all_movies() 27 | 28 | @movies.get('/{id}/', response_model=MovieOut) 29 | async def get_movie(id: int): 30 | movie = await db_manager.get_movie(id) 31 | if not movie: 32 | raise HTTPException(status_code=404, detail="Movie not found") 33 | return movie 34 | 35 | @movies.put('/{id}/', response_model=MovieOut) 36 | async def update_movie(id: int, payload: MovieUpdate): 37 | movie = await db_manager.get_movie(id) 38 | if not movie: 39 | raise HTTPException(status_code=404, detail="Movie not found") 40 | 41 | update_data = payload.dict(exclude_unset=True) 42 | 43 | if 'casts_id' in update_data: 44 | for cast_id in payload.casts_id: 45 | if not is_cast_present(cast_id): 46 | raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found") 47 | 48 | movie_in_db = MovieIn(**movie) 49 | 50 | updated_movie = movie_in_db.copy(update=update_data) 51 | 52 | return await db_manager.update_movie(id, updated_movie) 53 | 54 | @movies.delete('/{id}/', response_model=None) 55 | async def delete_movie(id: int): 56 | movie = await db_manager.get_movie(id) 57 | if not movie: 58 | raise HTTPException(status_code=404, detail="Movie not found") 59 | return await db_manager.delete_movie(id) 60 | -------------------------------------------------------------------------------- /movie-service/app/api/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import httpx 3 | 4 | CAST_SERVICE_HOST_URL = 'http://localhost:8002/api/v1/casts/' 5 | 6 | def is_cast_present(cast_id: int): 7 | url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL 8 | r = httpx.get(f'{url}{cast_id}') 9 | return True if r.status_code == 200 else False -------------------------------------------------------------------------------- /movie-service/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from app.api.movies import movies 3 | from app.api.db import metadata, database, engine 4 | 5 | metadata.create_all(engine) 6 | 7 | app = FastAPI(openapi_url="/api/v1/movies/openapi.json", docs_url="/api/v1/movies/docs") 8 | 9 | @app.on_event("startup") 10 | async def startup(): 11 | await database.connect() 12 | 13 | @app.on_event("shutdown") 14 | async def shutdown(): 15 | await database.disconnect() 16 | 17 | 18 | app.include_router(movies, prefix='/api/v1/movies', tags=['movies']) -------------------------------------------------------------------------------- /movie-service/requirements.txt: -------------------------------------------------------------------------------- 1 | asyncpg==0.20.1 2 | databases[postgresql]==0.2.6 3 | fastapi==0.48.0 4 | SQLAlchemy==1.3.13 5 | uvicorn==0.11.2 6 | httpx==0.11.1 -------------------------------------------------------------------------------- /nginx_config.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | 4 | location /api/v1/movies { 5 | proxy_pass http://movie_service:8000/api/v1/movies; 6 | } 7 | 8 | location /api/v1/casts { 9 | proxy_pass http://cast_service:8000/api/v1/casts; 10 | } 11 | 12 | } --------------------------------------------------------------------------------