api 서버 및 샘플 percona 서버 설정 & user CRUD 구현
parent
aefab0130c
commit
f9a30d6b39
|
|
@ -123,6 +123,7 @@ celerybeat.pid
|
|||
# Environments
|
||||
.env
|
||||
.venv
|
||||
.venv/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
|
|
|||
20
README.md
20
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
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
MYSQL_HOST="percona-server"
|
||||
MYSQL_USER="aio2o"
|
||||
MYSQL_PASSWORD="yourpasswordhere"
|
||||
MYSQL_ROOT_PASSWORD="yourrootpasswordhere"
|
||||
MYSQL_DATABASE="yourdbnamehere"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker exec -it fastapi-server /bin/bash
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
dotenv
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
mysql-connector-python
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
MYSQL_USER="aio2o"
|
||||
MYSQL_PASSWORD="yourpasswordhere"
|
||||
MYSQL_ROOT_PASSWORD="yourrootpasswordhere"
|
||||
MYSQL_DATABASE="yourdbnamehere"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker exec -it percona-server mysql -u aio2o -p
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fastapi
|
||||
uvicorn[standard]
|
||||
mysql-connector-python
|
||||
Loading…
Reference in New Issue