├── Thanaphoom-Babparn-PromptPayQR.jpg ├── cast_service ├── requirements.txt ├── Dockerfile ├── app │ ├── api │ │ ├── models.py │ │ ├── db_manager.py │ │ ├── db.py │ │ └── casts.py │ └── main.py └── README.md ├── example-pictures ├── Cast Service OpenAPI.png ├── Movie Service OpenAPI.png ├── Cast Service - Create Caster.png ├── Movie Service - Create Movie.png ├── Cast Service - Get Caster Detail.png └── Movie Service - Get Movie Detail.png ├── movie_service ├── requirements.txt ├── Dockerfile ├── app │ ├── api │ │ ├── service.py │ │ ├── models.py │ │ ├── db.py │ │ ├── db_manager.py │ │ └── movies.py │ └── main.py └── README.md ├── nginx_config.conf ├── README.md ├── docker-compose.yml └── .gitignore /Thanaphoom-Babparn-PromptPayQR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/Thanaphoom-Babparn-PromptPayQR.jpg -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /example-pictures/Cast Service OpenAPI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Cast Service OpenAPI.png -------------------------------------------------------------------------------- /example-pictures/Movie Service OpenAPI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Movie Service OpenAPI.png -------------------------------------------------------------------------------- /example-pictures/Cast Service - Create Caster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Cast Service - Create Caster.png -------------------------------------------------------------------------------- /example-pictures/Movie Service - Create Movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Movie Service - Create Movie.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /example-pictures/Cast Service - Get Caster Detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Cast Service - Get Caster Detail.png -------------------------------------------------------------------------------- /example-pictures/Movie Service - Get Movie Detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marttp/fastapi-microservice/HEAD/example-pictures/Movie Service - Get Movie Detail.png -------------------------------------------------------------------------------- /nginx_config.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 8080; 4 | 5 | location /api/v1/movies { 6 | proxy_pass http://movie_service:8000/api/v1/movies; 7 | } 8 | 9 | location /api/v1/casts { 10 | proxy_pass http://cast_service:8000/api/v1/casts; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /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/ -------------------------------------------------------------------------------- /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/ -------------------------------------------------------------------------------- /cast_service/app/api/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | 4 | 5 | class CastIn(BaseModel): 6 | name: str 7 | nationality: Optional[str] = None 8 | 9 | 10 | class CastOut(CastIn): 11 | id: int 12 | 13 | 14 | class CastUpdate(CastIn): 15 | name: Optional[str] = None 16 | -------------------------------------------------------------------------------- /movie_service/app/api/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import httpx 3 | 4 | CAST_SERVICE_HOST_URL = 'http://localhost:8082/api/v1/casts/' 5 | 6 | 7 | def is_cast_present(cast_id: int): 8 | url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL 9 | r = httpx.get(f'{url}{cast_id}') 10 | return True if r.status_code == 200 else False 11 | -------------------------------------------------------------------------------- /cast_service/README.md: -------------------------------------------------------------------------------- 1 | Create Virtual Environment for Python project 2 | 3 | **python -m venv venv** 4 | 5 | --- 6 | Activate venv 7 | 8 | Windows: .\venv\Scripts\activate 9 | 10 | Linux: source ./venv/bin/activate 11 | 12 | --- 13 | Install dependencies 14 | 15 | pip install -r requirements.txt 16 | 17 | --- 18 | Start Server 19 | 20 | uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -------------------------------------------------------------------------------- /movie_service/README.md: -------------------------------------------------------------------------------- 1 | Create Virtual Environment for Python project 2 | 3 | **python -m venv venv** 4 | 5 | --- 6 | Activate venv 7 | 8 | Windows: .\venv\Scripts\activate 9 | 10 | Linux: source ./venv/bin/activate 11 | 12 | --- 13 | Install dependencies 14 | 15 | pip install -r requirements.txt 16 | 17 | --- 18 | Start Server 19 | 20 | uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -------------------------------------------------------------------------------- /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 | 11 | async def get_cast(id): 12 | query = casts.select(casts.c.id == id) 13 | return await database.fetch_one(query=query) 14 | -------------------------------------------------------------------------------- /movie_service/app/api/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | 4 | 5 | class MovieIn(BaseModel): 6 | name: str 7 | plot: str 8 | genres: List[str] 9 | casts_id: List[int] 10 | 11 | 12 | class MovieOut(MovieIn): 13 | id: int 14 | 15 | 16 | class MovieUpdate(MovieIn): 17 | name: Optional[str] = None 18 | plot: Optional[str] = None 19 | genres: Optional[List[str]] = None 20 | casts_id: Optional[List[int]] = None 21 | -------------------------------------------------------------------------------- /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 | 10 | @app.on_event("startup") 11 | async def startup(): 12 | await database.connect() 13 | 14 | 15 | @app.on_event("shutdown") 16 | async def shutdown(): 17 | await database.disconnect() 18 | 19 | 20 | app.include_router(casts, prefix='/api/v1/casts', tags=['casts']) 21 | -------------------------------------------------------------------------------- /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) 22 | -------------------------------------------------------------------------------- /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 | 10 | @app.on_event("startup") 11 | async def startup(): 12 | await database.connect() 13 | 14 | 15 | @app.on_event("shutdown") 16 | async def shutdown(): 17 | await database.disconnect() 18 | 19 | 20 | app.include_router(movies, prefix='/api/v1/movies', tags=['movies']) 21 | -------------------------------------------------------------------------------- /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) 24 | -------------------------------------------------------------------------------- /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 | 10 | @casts.post('/', response_model=CastOut, status_code=201) 11 | async def create_cast(payload: CastIn): 12 | cast_id = await db_manager.add_cast(payload) 13 | 14 | response = { 15 | 'id': cast_id, 16 | **payload.dict() 17 | } 18 | 19 | return response 20 | 21 | 22 | @casts.get('/{id}/', response_model=CastOut) 23 | async def get_cast(id: int): 24 | cast = await db_manager.get_cast(id) 25 | if not cast: 26 | raise HTTPException(status_code=404, detail="Cast not found") 27 | return cast 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Microservice with FastAPI 2 | Side project to build Python microservices with FastAPI 3 | 4 | ## How to run project 5 | - Make sure you have installed `docker` and `docker-compose` 6 | - Run `docker-compose up -d` or `docker compose up -d` 7 | 8 | ## Shutdown services 9 | - Run `docker-compose down` or `docker compose down` 10 | 11 | ## Components 12 | - Nginx [PORT 8080] Use for reverse proxy to microservice 13 | - Movie Service MS [PORT 8001] 14 | - Cast Service MS [PORT 8002] 15 | 16 | ## สำหรับบุคคลชาวไทย 17 | สำหรับบุคคลที่เห็นว่า Repository นี้เป็นประโยชน์ หรือสามารถนำไปต่อยอดได้ 18 | ถ้าอยากจะสนับสนุน สามารถ Donate ได้ผ่านทางช่องทางนี้เลยครับ 19 | 20 | ### Prompt Pay QR 21 | Thanaphoom Babparn Promptpay QR Code cost 50 BAHT 22 | -------------------------------------------------------------------------------- /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 | 11 | async def get_all_movies(): 12 | query = movies.select() 13 | return await database.fetch_all(query=query) 14 | 15 | 16 | async def get_movie(id): 17 | query = movies.select(movies.c.id == id) 18 | return await database.fetch_one(query=query) 19 | 20 | 21 | async def delete_movie(id: int): 22 | query = movies.delete().where(movies.c.id == id) 23 | return await database.execute(query=query) 24 | 25 | 26 | async def update_movie(id: int, payload: MovieIn): 27 | query = ( 28 | movies 29 | .update() 30 | .where(movies.c.id == id) 31 | .values(**payload.dict()) 32 | ) 33 | return await database.execute(query=query) 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | # Movie Service 5 | movie_service: 6 | build: ./movie_service 7 | command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 8 | volumes: 9 | - ./movie_service/:/app/ 10 | ports: 11 | - 8001:8000 12 | environment: 13 | - DATABASE_URI=postgresql://movie_username:movie_password@movie_db/movie_db_dev 14 | - CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/ 15 | depends_on: 16 | - movie_db 17 | 18 | movie_db: 19 | image: postgres:12.1-alpine 20 | volumes: 21 | - postgres_data_movie:/var/lib/postgresql/data/ 22 | expose: 23 | - 5432 24 | environment: 25 | - POSTGRES_USER=movie_username 26 | - POSTGRES_PASSWORD=movie_password 27 | - POSTGRES_DB=movie_db_dev 28 | 29 | # Cast Service 30 | cast_service: 31 | build: ./cast_service 32 | command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 33 | volumes: 34 | - ./cast_service/:/app/ 35 | ports: 36 | - 8002:8000 37 | environment: 38 | - DATABASE_URI=postgresql://cast_username:cast_password@cast_db/cast_db_dev 39 | depends_on: 40 | - cast_db 41 | 42 | cast_db: 43 | image: postgres:12.1-alpine 44 | volumes: 45 | - postgres_data_cast:/var/lib/postgresql/data/ 46 | expose: 47 | - 5432 48 | environment: 49 | - POSTGRES_USER=cast_username 50 | - POSTGRES_PASSWORD=cast_password 51 | - POSTGRES_DB=cast_db_dev 52 | 53 | # Nginx 54 | nginx: 55 | image: nginx:latest 56 | ports: 57 | - "8080:8080" 58 | volumes: 59 | - ./nginx_config.conf:/etc/nginx/conf.d/default.conf 60 | depends_on: 61 | - cast_service 62 | - movie_service 63 | 64 | # Volumes 65 | volumes: 66 | postgres_data_movie: 67 | postgres_data_cast: -------------------------------------------------------------------------------- /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 | 11 | @movies.post('/', response_model=MovieOut, status_code=201) 12 | async def create_movie(payload: MovieIn): 13 | for cast_id in payload.casts_id: 14 | if not is_cast_present(cast_id): 15 | raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found") 16 | 17 | movie_id = await db_manager.add_movie(payload) 18 | response = { 19 | 'id': movie_id, 20 | **payload.dict() 21 | } 22 | 23 | return response 24 | 25 | 26 | @movies.get('/', response_model=List[MovieOut]) 27 | async def get_movies(): 28 | return await db_manager.get_all_movies() 29 | 30 | 31 | @movies.get('/{id}/', response_model=MovieOut) 32 | async def get_movie(id: int): 33 | movie = await db_manager.get_movie(id) 34 | if not movie: 35 | raise HTTPException(status_code=404, detail="Movie not found") 36 | return movie 37 | 38 | 39 | @movies.put('/{id}/', response_model=MovieOut) 40 | async def update_movie(id: int, payload: MovieUpdate): 41 | movie = await db_manager.get_movie(id) 42 | if not movie: 43 | raise HTTPException(status_code=404, detail="Movie not found") 44 | 45 | update_data = payload.dict(exclude_unset=True) 46 | 47 | if 'casts_id' in update_data: 48 | for cast_id in payload.casts_id: 49 | if not is_cast_present(cast_id): 50 | raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found") 51 | 52 | movie_in_db = MovieIn(**movie) 53 | 54 | updated_movie = movie_in_db.copy(update=update_data) 55 | 56 | return await db_manager.update_movie(id, updated_movie) 57 | 58 | 59 | @movies.delete('/{id}/', response_model=None) 60 | async def delete_movie(id: int): 61 | movie = await db_manager.get_movie(id) 62 | if not movie: 63 | raise HTTPException(status_code=404, detail="Movie not found") 64 | return await db_manager.delete_movie(id) 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Other 142 | venv 143 | .vscode 144 | .idea 145 | 146 | --------------------------------------------------------------------------------