From 154d7ca2bde15247efd5f9ff19e9cad91561ebe5 Mon Sep 17 00:00:00 2001 From: Yagiz Degirmenci Date: Thu, 6 Jan 2022 03:16:24 +0300 Subject: [PATCH] feat: add examples folder Signed-off-by: Yagiz Degirmenci --- examples/README.md | 100 +++++++++++++ examples/leaderboard/.gitignore | 141 ++++++++++++++++++ examples/leaderboard/Dockerfile | 13 ++ examples/leaderboard/LICENSE | 21 +++ examples/leaderboard/README.md | 7 + examples/leaderboard/app/__init__.py | 0 examples/leaderboard/app/api/__init__.py | 0 examples/leaderboard/app/api/v1/__init__.py | 0 examples/leaderboard/app/api/v1/crud.py | 64 ++++++++ examples/leaderboard/app/core/__init__.py | 0 examples/leaderboard/app/core/config.py | 40 +++++ .../leaderboard/app/core/models/__init__.py | 0 examples/leaderboard/app/core/models/model.py | 29 ++++ examples/leaderboard/app/database.py | 74 +++++++++ examples/leaderboard/app/main.py | 23 +++ examples/leaderboard/docker-compose.yml | 44 ++++++ examples/leaderboard/requirements.txt | 4 + examples/leaderboard/tests/__init__.py | 0 18 files changed, 560 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/leaderboard/.gitignore create mode 100644 examples/leaderboard/Dockerfile create mode 100644 examples/leaderboard/LICENSE create mode 100644 examples/leaderboard/README.md create mode 100644 examples/leaderboard/app/__init__.py create mode 100644 examples/leaderboard/app/api/__init__.py create mode 100644 examples/leaderboard/app/api/v1/__init__.py create mode 100644 examples/leaderboard/app/api/v1/crud.py create mode 100644 examples/leaderboard/app/core/__init__.py create mode 100644 examples/leaderboard/app/core/config.py create mode 100644 examples/leaderboard/app/core/models/__init__.py create mode 100644 examples/leaderboard/app/core/models/model.py create mode 100644 examples/leaderboard/app/database.py create mode 100644 examples/leaderboard/app/main.py create mode 100644 examples/leaderboard/docker-compose.yml create mode 100644 examples/leaderboard/requirements.txt create mode 100644 examples/leaderboard/tests/__init__.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..265401d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,100 @@ +## Example leaderboard project + +Example leaderboard project to demonstrate how to get started with templates. + +It includes 3 endpoints: + +## `/api/v1/user/create` + +### Request + +```json +HTTP POST /api/v1/user/create + +{ + "country": "tr", + "display_name": "yagu" +} +``` + +### Response + +```json +{ + "error": "", + "success": true, + "data": { + "user_id": "2be26a1d-ee4e-45b1-98fe-f66e3a224c9b" + } +} +``` + + +## `/api/v1/score/submit` + + +### Request + +```json +HTTP POST /api/v1/user/create + +{ + "user_id": "2be26a1d-ee4e-45b1-98fe-f66e3a224c9b", + "score": 43 +} +``` + +### Response + +```json +{ + "error": "", + "success": true, + "data": {} +} +``` + + + + +## `/api/v1/leaderboard` + +### Request + +```json +HTTP GET /api/v1/leaderboard +``` + +### Response + +```json +{ + "error": "", + "success": true, + "data": [ + { + "user_id": "7cacffca-3cd0-44e0-a7ef-d1675211a4ca", + "name": "yagu", + "country": "tr", + "points": 100, + "rank": 1 + }, + { + "user_id": "2be26a1d-ee4e-45b1-98fe-f66e3a224c9b", + "name": "yagu", + "country": "tr", + "points": 43, + "rank": 2 + } + ] +} +``` + + +### How to run the project? + + +1. Create the docker image with `docker build . -t leaderboard` +1. Run `docker-compose up` which will start the project +1. Check out the api at http://0.0.0.0:8000/docs + diff --git a/examples/leaderboard/.gitignore b/examples/leaderboard/.gitignore new file mode 100644 index 0000000..bad5d49 --- /dev/null +++ b/examples/leaderboard/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Text Editor +.vscode diff --git a/examples/leaderboard/Dockerfile b/examples/leaderboard/Dockerfile new file mode 100644 index 0000000..b15bf25 --- /dev/null +++ b/examples/leaderboard/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.9-slim + +RUN pip install --upgrade pip + +COPY ./requirements.txt /app/ + +RUN apt-get update \ + && apt-get install gcc -y \ + && apt-get clean + +RUN pip install -r app/requirements.txt + +COPY ./app . \ No newline at end of file diff --git a/examples/leaderboard/LICENSE b/examples/leaderboard/LICENSE new file mode 100644 index 0000000..6e3d293 --- /dev/null +++ b/examples/leaderboard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Yagiz Degirmenci + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/leaderboard/README.md b/examples/leaderboard/README.md new file mode 100644 index 0000000..bc0cb37 --- /dev/null +++ b/examples/leaderboard/README.md @@ -0,0 +1,7 @@ +# leaderboard + +This project was generated via [manage-fastapi](https://ycd.github.io/manage-fastapi/)! :tada: + +## License + +This project is licensed under the terms of the MIT license. diff --git a/examples/leaderboard/app/__init__.py b/examples/leaderboard/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/leaderboard/app/api/__init__.py b/examples/leaderboard/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/leaderboard/app/api/v1/__init__.py b/examples/leaderboard/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/leaderboard/app/api/v1/crud.py b/examples/leaderboard/app/api/v1/crud.py new file mode 100644 index 0000000..37b4e30 --- /dev/null +++ b/examples/leaderboard/app/api/v1/crud.py @@ -0,0 +1,64 @@ +from typing import Any +import random, time +from uuid import uuid4 +from pydantic.types import UUID1 + +from sqlalchemy.engine import create_engine +from app.app.core.models.model import Score, User, UserCreate +from fastapi import APIRouter +from pydantic import BaseModel +from app.app.database import ( + database, + user_table, + score_table, + CREATE_LEADERBOARD_VIEW, + CREATE_USERS_WITH_SCORES_VIEW, + GET_LEADERBOARD, + CREATE_SCORE_TABLE, + CREATE_USERS_TABLE, +) + +router = APIRouter() + + +@router.on_event("startup") +async def connect_database(): + await database.connect() + await database.execute(CREATE_USERS_TABLE) + await database.execute(CREATE_SCORE_TABLE) + await database.execute(CREATE_USERS_WITH_SCORES_VIEW) + await database.execute(CREATE_LEADERBOARD_VIEW) + + +@router.on_event("shutdown") +async def disconnect_database(): + await database.disconnect() + + +class Response(BaseModel): + error: str + success: bool + data: Any + + +@router.post("/user/create") +async def create_user(user: UserCreate): + user_id = str(uuid4())[:40] + query = user_table.insert() + values = {"user_id": user_id, "name": user.display_name, "country": user.country} + await database.execute(query=query, values=values) + return Response(error="", success=True, data={"user_id": user_id}) + + +@router.get("/leaderboard") +async def leaderboard(): + rows = await database.fetch_all(GET_LEADERBOARD) + return Response(success=True, error="", data=rows) + + +@router.post("/score/submit") +async def submit_score(score: Score): + query = score_table.insert() + values = {"user_id": score.user_id, "point": score.score} + await database.execute(query=query, values=values) + return Response(error="", success=True, data={}) \ No newline at end of file diff --git a/examples/leaderboard/app/core/__init__.py b/examples/leaderboard/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/leaderboard/app/core/config.py b/examples/leaderboard/app/core/config.py new file mode 100644 index 0000000..c69a67a --- /dev/null +++ b/examples/leaderboard/app/core/config.py @@ -0,0 +1,40 @@ +from typing import Any, Dict, List, Optional, Union + +from pydantic import AnyHttpUrl, BaseSettings, validator + + +class Settings(BaseSettings): + PROJECT_NAME: str + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] + + @validator("BACKEND_CORS_ORIGINS", pre=True) + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, (list, str)): + return v + raise ValueError(v) + + MYSQL_USER: str + MYSQL_PASSWORD: str + MYSQL_HOST: str + MYSQL_PORT: str + MYSQL_DATABASE: str + DATABASE_URI: Optional[str] = None + + @validator("DATABASE_URI", pre=True) + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: + if isinstance(v, str): + return v + return ( + f"mysql://{values.get('MYSQL_USER')}:{values.get('MYSQL_PASSWORD')}@{values.get('MYSQL_HOST')}:" + f"{values.get('MYSQL_PORT')}/{values.get('MYSQL_DATABASE')}" + ) + + class Config: + case_sensitive = True + env_file = "../env" + + +settings = Settings() +print(settings.DATABASE_URI) diff --git a/examples/leaderboard/app/core/models/__init__.py b/examples/leaderboard/app/core/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/leaderboard/app/core/models/model.py b/examples/leaderboard/app/core/models/model.py new file mode 100644 index 0000000..d5881a8 --- /dev/null +++ b/examples/leaderboard/app/core/models/model.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel +from typing import List + + +class UserCreate(BaseModel): + country: str + display_name: str + + +class User(BaseModel): + user_id: str + country: str + display_name: str + + +class Score(BaseModel): + user_id: str + score: int + + +class UserScore(BaseModel): + user_id: str + score: int + country: str + display_name: str + + +class Leaderboard(BaseModel): + data: List[UserScore] diff --git a/examples/leaderboard/app/database.py b/examples/leaderboard/app/database.py new file mode 100644 index 0000000..a06b3c4 --- /dev/null +++ b/examples/leaderboard/app/database.py @@ -0,0 +1,74 @@ +from sqlalchemy.engine import create_engine +from core.config import settings +from databases import Database +import sqlalchemy + + +metadata = sqlalchemy.MetaData() + +user_table = sqlalchemy.Table( + "users", + metadata, + sqlalchemy.Column("user_id", sqlalchemy.String(40), primary_key=True), + sqlalchemy.Column("name", sqlalchemy.String(50), nullable=False), + sqlalchemy.Column("country", sqlalchemy.String(50), nullable=False), +) + +score_table = sqlalchemy.Table( + "scores", + metadata, + sqlalchemy.Column("user_id", sqlalchemy.Integer), + sqlalchemy.Column("point", sqlalchemy.Integer, nullable=False), +) + +CREATE_USERS_TABLE = """ +CREATE TABLE IF NOT EXISTS users ( + user_id VARCHAR(40) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + country VARCHAR(50) NOT NULL +) +""" + +CREATE_SCORE_TABLE = """ +CREATE TABLE IF NOT EXISTS scores ( + user_id VARCHAR(40) NOT NULL, + point INTEGER NOT NULL +) +""" + + +CREATE_USERS_WITH_SCORES_VIEW = """ +CREATE OR REPLACE VIEW users_with_scores AS +SELECT + u.user_id, + u.name, + u.country, + ( + SELECT COALESCE(SUM(s.point), 0) + FROM scores s + WHERE u.user_id=s.user_id + ) AS points +FROM users u +""" + +CREATE_LEADERBOARD_VIEW = """ +CREATE OR REPLACE VIEW leaderboard AS SELECT *, DENSE_RANK() OVER (ORDER BY points desc) AS "rank" +FROM users_with_scores +""" + +# write an mysql query that creates a view called leaderboard +# it queries everything from users_with_scores table and adds a rank column +# the rank will be calculated by sorting the users by points in descending order +# https://www.youtube.com/watch?v=0sZ_JkQs1xk&t=10s + +# the query should look like this: + + +GET_LEADERBOARD = """ +SELECT * +FROM leaderboard l +ORDER BY l.rank +""" + + +database = Database(settings.DATABASE_URI) \ No newline at end of file diff --git a/examples/leaderboard/app/main.py b/examples/leaderboard/app/main.py new file mode 100644 index 0000000..36804cf --- /dev/null +++ b/examples/leaderboard/app/main.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.app.api.v1.crud import router as crud_router +from core.config import settings + + +def get_application(): + _app = FastAPI(title=settings.PROJECT_NAME) + + _app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + return _app + + +app = get_application() +app.include_router(crud_router, prefix="/api/v1") diff --git a/examples/leaderboard/docker-compose.yml b/examples/leaderboard/docker-compose.yml new file mode 100644 index 0000000..646f870 --- /dev/null +++ b/examples/leaderboard/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.9" +services: + web: + image: leaderboard:latest + restart: always + command: uvicorn app.app.main:app --host 0.0.0.0 --port 8000 + volumes: + - .:/app/ + networks: + - backend + depends_on: + - mysql + environment: + - PROJECT_NAME=leaderboard + - BACKEND_CORS_ORIGINS=["http://localhost:8000","https://localhost:8000","http://localhost","https://localhost"] + - MYSQL_ROOT_PASSWORD=super_secret_password + - MYSQL_DATABASE=leaderboard + - MYSQL_USER=yagu + - MYSQL_PASSWORD=super_secret_password + - MYSQL_PORT=3306 + - MYSQL_HOST=mysql + ports: + - "8000:8000" + + mysql: + image: mysql + restart: always + networks: + - backend + environment: + MYSQL_ROOT_PASSWORD: super_secret_password + MYSQL_DATABASE: leaderboard + MYSQL_USER: yagu + MYSQL_PASSWORD: super_secret_password + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + timeout: 5s + retries: 20 + ports: + - "3306:3306" + +networks: + backend: + driver: bridge diff --git a/examples/leaderboard/requirements.txt b/examples/leaderboard/requirements.txt new file mode 100644 index 0000000..9cc1487 --- /dev/null +++ b/examples/leaderboard/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.68.0 +uvicorn==0.12.2 +databases[mysql]==0.5.3 +cryptography \ No newline at end of file diff --git a/examples/leaderboard/tests/__init__.py b/examples/leaderboard/tests/__init__.py new file mode 100644 index 0000000..e69de29