Compare commits
2 Commits
77fc0ca24d
...
f9a30d6b39
| Author | SHA1 | Date |
|---|---|---|
|
|
f9a30d6b39 | |
|
|
aefab0130c |
|
|
@ -123,6 +123,7 @@ celerybeat.pid
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
.venv/
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -1,2 +1,23 @@
|
||||||
# aio2o-fastapi-sample
|
# aio2o-fastapi-sample
|
||||||
|
|
||||||
|
# Requirement
|
||||||
|
Docker, Docker-compose
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
### Percona 설치
|
||||||
|
./percona-server
|
||||||
|
.env.original 파일 보고 변수 원하는대로 바꿔 .env로 저장
|
||||||
|
```
|
||||||
|
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
|
||||||
Loading…
Reference in New Issue