diff --git a/.gitignore b/.gitignore index 33e7851..b3994dd 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ celerybeat.pid # Environments .env .venv +.venv/ env/ venv/ ENV/ diff --git a/README.md b/README.md index 277155c..9377a7e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,23 @@ # aio2o-fastapi-sample +# Requirement +Docker, Docker-compose + ## install +### Percona 설치 +./percona-server +.env.original 파일 보고 변수 원하는대로 바꿔 .env로 저장 ``` -pip install -r requirements.txt -``` \ No newline at end of file +docker compose -f docker-compose.percona.yml up -d +``` +### API Server 켜기 +./api-server +해당 폴더에도 따로 .env.original이 있으니 percona와 동일하게 변경하고 .env로 저장 +Percona 서버를 사용하지 않을 시 해당하는 서버에 맞게 저장 + +``` +docker compose -f docker-compose.yml build +docker compose -f docker-compose.yml up -d +``` + diff --git a/api-server/Dockerfile b/api-server/Dockerfile new file mode 100644 index 0000000..9f16ec7 --- /dev/null +++ b/api-server/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12 + +RUN apt-get update && apt-get install -y locales +RUN locale-gen ko_KR.UTF-8 +RUN apt-get update && apt-get install git -y + +WORKDIR /app + +COPY ./requirements.txt /requirements.txt +RUN pip install --no-cache-dir --upgrade -r /requirements.txt \ No newline at end of file diff --git a/api-server/app/.env.original b/api-server/app/.env.original new file mode 100644 index 0000000..ef7adb6 --- /dev/null +++ b/api-server/app/.env.original @@ -0,0 +1,5 @@ +MYSQL_HOST="percona-server" +MYSQL_USER="aio2o" +MYSQL_PASSWORD="yourpasswordhere" +MYSQL_ROOT_PASSWORD="yourrootpasswordhere" +MYSQL_DATABASE="yourdbnamehere" \ No newline at end of file diff --git a/api-server/app/main.py b/api-server/app/main.py new file mode 100644 index 0000000..1448799 --- /dev/null +++ b/api-server/app/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, status +from fastapi.responses import JSONResponse +from module.pydantic_models import * +import module.mysql_utils as sql + +app = FastAPI() + +@app.post("/user/create", status_code=status.HTTP_201_CREATED) +async def create_user(params: UserCreateForm): + user_id = await sql.create_user(params.user_name, params.phone_number) + return JSONResponse(content={'user_id' : user_id}) + +@app.post("/user/read", status_code=status.HTTP_200_OK) +async def read_user(params: UserReadFromIdForm): + user_info = await sql.get_user_info_from_id(params.user_id) + return JSONResponse(content=user_info) + +@app.post("/user/update", status_code=status.HTTP_200_OK) +async def update_user(params: UserUpdatePhoneNumberFromIdForm): + updated_data = await sql.update_user_phone_from_id(params.user_id, params.phone_number) + return updated_data + +@app.post("/user/delete", status_code=status.HTTP_204_NO_CONTENT) +async def delete_user(params: UserDeleteForm): + await sql.delete_user_from_id(params.user_id) + return + +@app.post("/blog/create") +async def create_blog(params: BlogCreateForm): + pass +@app.post("/blog/read") +async def read_blog(params: BlogReadForm): + pass +@app.post("/blog/update") +async def update_blog(params: BlogUpdateForm): + pass +@app.post("/blog/delete") +async def delete_blog(params: BlogDeleteForm): + pass \ No newline at end of file diff --git a/api-server/app/module/mysql_utils.py b/api-server/app/module/mysql_utils.py new file mode 100644 index 0000000..e5d123b --- /dev/null +++ b/api-server/app/module/mysql_utils.py @@ -0,0 +1,126 @@ +import asyncio +import os +import pydantic +from dotenv import load_dotenv +from mysql.connector.aio import connect +from mysql.connector.errors import IntegrityError +from fastapi import HTTPException, status +load_dotenv(verbose=True) + +async def get_cnx(): + cnx = await connect( + host = os.getenv("MYSQL_HOST"), + user = os.getenv("MYSQL_USER"), + password = os.getenv("MYSQL_PASSWORD"), + database = os.getenv("MYSQL_DATABASE"), + ) + return cnx + +async def create_user(user_name:str, phone_number:str=None): + async with await get_cnx() as cnx: + async with await cnx.cursor() as cur: + query = ''' + INSERT INTO user_table (user_name, phone_number) + VALUES (%(user_name)s, %(phone_number)s) + ''' + data = { + "user_name" : user_name, + "phone_number" : phone_number + } + try: + await cur.execute(query, data) + except IntegrityError as e: + await cnx.rollback() + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="user already exist") + + user_id = cur.lastrowid + print(user_id) + await cnx.commit() + return user_id + +async def get_user_info_from_id(user_id:int): + async with await get_cnx() as cnx: + async with await cnx.cursor() as cur: + query = ''' + SELECT user_name, phone_number FROM user_table + WHERE user_id=%(user_id)s + ''' + data = {"user_id" : user_id} + + await cur.execute(query, data) + user_info = await cur.fetchone() + await cnx.commit() + if not user_info: + raise HTTPException(status.HTTP_404_NOT_FOUND) + return user_info + +async def get_user_id_from_name(user_name:str): + async with await get_cnx() as cnx: + async with await cnx.cursor() as cur: + query = ''' + SELECT user_id FROM user_table + WHERE user_name=%(user_name)s + ''' + data = {"user_name" : user_name} + await cur.execute(query, data) + user_info = await cur.fetchone() + await cnx.commit() + if not user_info: + raise HTTPException(status.HTTP_404_NOT_FOUND) + return user_info["user_id"] + +async def update_user_phone_from_id(user_id:int, phone_number:str=None): + async with await get_cnx() as cnx: + async with await cnx.cursor() as cur: + count_query = ''' + SELECT count(*) + FROM user_table + WHERE user_id = %(user_id)s + ''' + await cur.execute(count_query, {'user_id' : user_id}) + found_rows = await cur.fetchone() + if found_rows[0] == 0: + await cnx.rollback() + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + query = ''' + UPDATE user_table + SET phone_number = %(phone_number)s + WHERE user_id = %(user_id)s + ''' + data = { + "user_id" : user_id, + "phone_number" : phone_number + } + + await cur.execute(query, data) + + if cur.rowcount == 0: + await cnx.rollback() + raise HTTPException(status_code=status.HTTP_204_NO_CONTENT) + + await cnx.commit() + return data + + +async def delete_user_from_id(user_id:int): + async with await get_cnx() as cnx: + async with await cnx.cursor() as cur: + query = ''' + DELETE + FROM user_table + WHERE user_id = %(user_id)s + ''' + data = { + "user_id" : user_id + } + try: + await cur.execute(query, data) + except IntegrityError as e: + await cnx.rollback() + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="user already exist") + await cnx.commit() + return + + + diff --git a/api-server/app/module/pydantic_models.py b/api-server/app/module/pydantic_models.py new file mode 100644 index 0000000..0cd443a --- /dev/null +++ b/api-server/app/module/pydantic_models.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel + +class UserCreateForm(BaseModel): + user_name:str + phone_number:str + +class UserReadFromIdForm(BaseModel): + user_id:int + +class UserUpdatePhoneNumberFromIdForm(BaseModel): + user_id:int + phone_number:str + +class UserDeleteForm(BaseModel): + user_id:int + + +class BlogCreateForm(BaseModel): + owner:int + blog_title:str + blog_content:str + +class BlogReadForm(BaseModel): + owner:int + blog_id:int + +class BlogUpdateForm(BaseModel): + owner:int + blog_id:int + blog_title:str + blog_content:str + +class BlogDeleteForm(BaseModel): + owner:int + blog_id:int \ No newline at end of file diff --git a/api-server/docker-compose.yml b/api-server/docker-compose.yml new file mode 100644 index 0000000..6f52708 --- /dev/null +++ b/api-server/docker-compose.yml @@ -0,0 +1,18 @@ +services: + fastapi: + build: . + container_name: fastapi-server + command: uvicorn main:app --host 0.0.0.0 --port 8080 --reload + ports: + - 8080:8080 + networks: + - test_app_network + volumes: + - ./app:/app + +networks: + test_app_network: + name: test_app_network + driver: bridge + external: true + \ No newline at end of file diff --git a/api-server/gogo b/api-server/gogo new file mode 100755 index 0000000..9c2af28 --- /dev/null +++ b/api-server/gogo @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -it fastapi-server /bin/bash \ No newline at end of file diff --git a/api-server/requirements.txt b/api-server/requirements.txt new file mode 100644 index 0000000..7ae94da --- /dev/null +++ b/api-server/requirements.txt @@ -0,0 +1,4 @@ +dotenv +fastapi +uvicorn[standard] +mysql-connector-python \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index e69de29..0000000 diff --git a/module/mysql_utils.py b/module/mysql_utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/percona-server/.env.original b/percona-server/.env.original new file mode 100644 index 0000000..3aa2544 --- /dev/null +++ b/percona-server/.env.original @@ -0,0 +1,4 @@ +MYSQL_USER="aio2o" +MYSQL_PASSWORD="yourpasswordhere" +MYSQL_ROOT_PASSWORD="yourrootpasswordhere" +MYSQL_DATABASE="yourdbnamehere" \ No newline at end of file diff --git a/percona-server/docker-compose.percona.yml b/percona-server/docker-compose.percona.yml new file mode 100644 index 0000000..9502aa8 --- /dev/null +++ b/percona-server/docker-compose.percona.yml @@ -0,0 +1,29 @@ +services: + mysql: + image: percona/percona-server:8.0 + container_name: percona-server + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - percona-data:/var/lib/mysql # Database data + - percona-logs:/var/log/mysql # MySQL logs + - percona-backups:/backups # XtraBackup output (optional, for future use) + restart: unless-stopped + networks: + - test_app_network + +volumes: + percona-data: + percona-logs: + percona-backups: + +networks: + test_app_network: + name: test_app_network + driver: bridge + diff --git a/percona-server/gosql b/percona-server/gosql new file mode 100755 index 0000000..61662a1 --- /dev/null +++ b/percona-server/gosql @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -it percona-server mysql -u aio2o -p \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 832ff73..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -uvicorn[standard] -mysql-connector-python \ No newline at end of file