first commit
This commit is contained in:
22
MYSQL/docker-compose.yml
Executable file
22
MYSQL/docker-compose.yml
Executable file
@@ -0,0 +1,22 @@
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
container_name: mysql
|
||||
restart: always
|
||||
environment:
|
||||
TZ: Asia/Seoul
|
||||
MYSQL_ROOT_PASSWORD: 1234
|
||||
MYSQL_DATABASE: medical_metaverse
|
||||
MYSQL_USER: medical_metaverse
|
||||
MYSQL_PASSWORD: 1234
|
||||
# healthcheck:
|
||||
# test: ["CMD", "echo 'SELECT version();'| mysql"]
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
volumes:
|
||||
- /MYSQL/data/:/var/lib/mysql
|
||||
- /MYSQL/backup:/backupfiles
|
||||
ports:
|
||||
- 3306:3306
|
||||
@@ -1,3 +1,3 @@
|
||||
# A2TEC_METAVERSE_HOSPITAL_REST_SERVER
|
||||
# A2TEC_METAVERSE_MEDICAL_REST_SERVER
|
||||
|
||||
A2TEC_METAVERSE_HOSPITAL_REST_SERVER
|
||||
A2TEC_METAVERSE_MEDICAL_REST_SERVER
|
||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
api:
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
container_name: metaverse_medical_rest
|
||||
image: metaverse/medical_rest:latest
|
||||
build:
|
||||
context: ./fast_api/
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
volumes:
|
||||
- ./fast_api:/FAST_API
|
||||
ports:
|
||||
- 50510-50532:50510-50532
|
||||
command: ["uvicorn", "app.main:app", "--host", '0.0.0.0', "--port", "50510"] #local
|
||||
# command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50532"] #my
|
||||
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
container_name: mysql
|
||||
restart: always
|
||||
environment:
|
||||
TZ: Asia/Seoul
|
||||
MYSQL_ROOT_PASSWORD: 1234
|
||||
MYSQL_DATABASE: medical_metaverse
|
||||
MYSQL_USER: medical_metaverse
|
||||
MYSQL_PASSWORD: 1234
|
||||
healthcheck:
|
||||
test: "mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e \"SHOW DATABASES;\""
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
volumes:
|
||||
- ./MYSQL/data/:/var/lib/mysql
|
||||
- ./MYSQL/backup:/backupfiles
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
9
fast_api/.dockerignore
Executable file
9
fast_api/.dockerignore
Executable file
@@ -0,0 +1,9 @@
|
||||
.git/
|
||||
.gitignore
|
||||
.idea/
|
||||
README.md
|
||||
Dockerfile
|
||||
__pycache__
|
||||
docker-compose.yml
|
||||
venv/
|
||||
tests/
|
||||
49
fast_api/.travis.yml
Executable file
49
fast_api/.travis.yml
Executable file
@@ -0,0 +1,49 @@
|
||||
os: linux
|
||||
dist: bionic
|
||||
language: python
|
||||
services:
|
||||
- docker
|
||||
- mysql
|
||||
python:
|
||||
- 3.8
|
||||
before_install:
|
||||
- pip install awscli
|
||||
- export PATH=$PATH:$HOME/.local/bin
|
||||
script:
|
||||
- pytest -v
|
||||
before_deploy:
|
||||
- if [ $TRAVIS_BRANCH == "master" ]; then export EB_ENV=notification-prd-api; fi
|
||||
- if [ $TRAVIS_BRANCH == "develop" ]; then export EB_ENV=notification-dev-api; fi
|
||||
- export REPO_NAME=$(echo $TRAVIS_REPO_SLUG | sed "s_^.*/__")
|
||||
- export ELASTIC_BEANSTALK_LABEL=${REPO_NAME}-${TRAVIS_COMMIT::7}-$(date +%y%m%d%H%M%S)
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: elasticbeanstalk
|
||||
access_key_id: $AWS_ACCESS
|
||||
secret_access_key: $AWS_SECRET
|
||||
region: ap-northeast-2
|
||||
bucket: elasticbeanstalk-ap-northeast-2-884465654078
|
||||
bucket_path: notification-api
|
||||
app: notification-api
|
||||
env: $EB_ENV
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_BRANCH =~ ^develop|master
|
||||
|
||||
#notifications:
|
||||
# slack:
|
||||
# - rooms:
|
||||
# - secure: ***********
|
||||
# if: branch = master
|
||||
# template:
|
||||
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."
|
||||
# - rooms:
|
||||
# - secure: ***********
|
||||
# if: branch = staging
|
||||
# template:
|
||||
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."
|
||||
# - rooms:
|
||||
# - secure: ***********
|
||||
# if: branch = develop
|
||||
# template:
|
||||
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."
|
||||
32
fast_api/Dockerfile
Executable file
32
fast_api/Dockerfile
Executable file
@@ -0,0 +1,32 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Base image
|
||||
# ------------------------------------------------------------------------------
|
||||
FROM python:3.9.7-slim
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Informations
|
||||
# ------------------------------------------------------------------------------
|
||||
LABEL maintainer="hsj100 <hsj100@a2tec.co.kr>"
|
||||
LABEL title="A2TEC_METAVERSER_MEDICAL_REST"
|
||||
LABEL description="Rest API Server with Fast API"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Source
|
||||
# ------------------------------------------------------------------------------
|
||||
COPY ./app /FAST_API/app
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Install dependencies
|
||||
# ------------------------------------------------------------------------------
|
||||
COPY ./requirements.txt /FAST_API/requirements.txt
|
||||
|
||||
WORKDIR /FAST_API
|
||||
RUN apt update > /dev/null && \
|
||||
apt install -y build-essential && \
|
||||
pip install --no-cache-dir --upgrade -r /FAST_API/requirements.txt
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Binary
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50210"]
|
||||
43
fast_api/README.md
Normal file
43
fast_api/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# METAVERSE_MEDICAL_REST_SERVER
|
||||
|
||||
RESTful API Server
|
||||
|
||||
|
||||
### System Requirements
|
||||
- Ubuntu 20.04
|
||||
- docker 20.10.9
|
||||
- docker-compose 1.25.0
|
||||
- python 3.9
|
||||
- python packages: requirements.txt
|
||||
|
||||
|
||||
### Docker
|
||||
- Base Image
|
||||
* python:3.9.7-slim
|
||||
|
||||
|
||||
- Target Image
|
||||
* medical_metaverse_restapi:latest
|
||||
|
||||
|
||||
- Environments variable
|
||||
|
||||
|
||||
- Build
|
||||
```
|
||||
docker-build.sh
|
||||
```
|
||||
|
||||
|
||||
- Run
|
||||
* docker-compose.yml 파일의 환경변수(environment) 수정 후 아래 명령으로 도커 실행
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
|
||||
### LOG
|
||||
- 서버 로그 파일
|
||||
```
|
||||
TBD
|
||||
```
|
||||
46
fast_api/app/api_request_sample.py
Executable file
46
fast_api/app/api_request_sample.py
Executable file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: api_request_sample.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: test api key
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def parse_params_to_str(params):
|
||||
url = "?"
|
||||
for key, value in params.items():
|
||||
url = url + str(key) + '=' + str(value) + '&'
|
||||
return url[1:-1]
|
||||
|
||||
|
||||
def hash_string(qs, secret_key):
|
||||
mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(qs, encoding='utf-8'), digestmod='sha256')
|
||||
d = mac.digest()
|
||||
validating_secret = str(base64.b64encode(d).decode('utf-8'))
|
||||
return validating_secret
|
||||
|
||||
|
||||
def sample_request():
|
||||
access_key = 'c0883231-4aa9-4a1f-a77b-3ef250af-e449-42e9-856a-b3ada17c426b'
|
||||
secret_key = 'QhOaeXTAAkW6yWt31jWDeERkBsZ3X4UmPds656YD'
|
||||
cur_time = datetime.utcnow()+timedelta(hours=9)
|
||||
cur_timestamp = int(cur_time.timestamp())
|
||||
qs = dict(key=access_key, timestamp=cur_timestamp)
|
||||
header_secret = hash_string(parse_params_to_str(qs), secret_key)
|
||||
|
||||
url = f'http://127.0.0.1:8080/api/services?{parse_params_to_str(qs)}'
|
||||
res = requests.get(url, headers=dict(secret=header_secret))
|
||||
return res
|
||||
|
||||
|
||||
print(sample_request().json())
|
||||
110
fast_api/app/common/config.py
Executable file
110
fast_api/app/common/config.py
Executable file
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: config.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: Configurations
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from os import path, environ
|
||||
|
||||
from app.common import consts
|
||||
from app.models import UserInfo
|
||||
|
||||
|
||||
base_dir = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""
|
||||
기본 Configuration
|
||||
"""
|
||||
BASE_DIR: str = base_dir
|
||||
DB_POOL_RECYCLE: int = 900
|
||||
DB_ECHO: bool = True
|
||||
DEBUG: bool = False
|
||||
TEST_MODE: bool = False
|
||||
DEV_TEST_CONNECT_ACCOUNT: str = None
|
||||
|
||||
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
|
||||
SERVICE_AUTH_API_KEY: bool = False
|
||||
|
||||
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}?charset={consts.DB_CHARSET}')
|
||||
REST_SERVER_PORT = consts.REST_SERVER_PORT
|
||||
|
||||
SW_TITLE = consts.SW_TITLE
|
||||
SW_VERSION = consts.SW_VERSION
|
||||
SW_DESCRIPTION = consts.SW_DESCRIPTION
|
||||
TERMS_OF_SERVICE = consts.TERMS_OF_SERVICE
|
||||
CONTEACT = consts.CONTEACT
|
||||
LICENSE_INFO = consts.LICENSE_INFO
|
||||
|
||||
# GLOBAL_TOKEN = consts.ADMIN_ACCOUNT_TOKEN
|
||||
GLOBAL_TOKEN = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalConfig(Config):
|
||||
TRUSTED_HOSTS = ['*']
|
||||
ALLOW_SITE = ['*']
|
||||
DEBUG: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProdConfig(Config):
|
||||
TRUSTED_HOSTS = ['*']
|
||||
ALLOW_SITE = ['*']
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestConfig(Config):
|
||||
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_test?charset={consts.DB_CHARSET}')
|
||||
TRUSTED_HOSTS = ['*']
|
||||
ALLOW_SITE = ['*']
|
||||
TEST_MODE: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevConfig(Config):
|
||||
TRUSTED_HOSTS = ['*']
|
||||
ALLOW_SITE = ['*']
|
||||
DEBUG: bool = True
|
||||
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_dev?charset={consts.DB_CHARSET}')
|
||||
REST_SERVER_PORT = consts.REST_SERVER_PORT + 1
|
||||
|
||||
SW_TITLE = '[Dev] ' + consts.SW_TITLE
|
||||
|
||||
|
||||
@dataclass
|
||||
class MyConfig(Config):
|
||||
TRUSTED_HOSTS = ['*']
|
||||
ALLOW_SITE = ['*']
|
||||
DEBUG: bool = True
|
||||
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_my?charset={consts.DB_CHARSET}')
|
||||
REST_SERVER_PORT = consts.REST_SERVER_PORT + 2
|
||||
|
||||
# NOTE(hsj100): DEV_TEST_CONNECT_ACCOUNT
|
||||
DEV_TEST_CONNECT_ACCOUNT: UserInfo = UserInfo(**consts.ADMIN_INIT_ACCOUNT_INFO)
|
||||
# DEV_TEST_CONNECT_ACCOUNT: str = None
|
||||
|
||||
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
|
||||
SERVICE_AUTH_API_KEY: bool = False
|
||||
|
||||
SW_TITLE = '[My] ' + consts.SW_TITLE
|
||||
|
||||
|
||||
def conf():
|
||||
"""
|
||||
환경 불러오기
|
||||
:return:
|
||||
"""
|
||||
config = dict(prod=ProdConfig, local=LocalConfig, test=TestConfig, dev=DevConfig, my=MyConfig)
|
||||
return config[environ.get('API_ENV', 'local')]()
|
||||
return config[environ.get('API_ENV', 'dev')]()
|
||||
return config[environ.get('API_ENV', 'my')]()
|
||||
return config[environ.get('API_ENV', 'test')]()
|
||||
94
fast_api/app/common/consts.py
Executable file
94
fast_api/app/common/consts.py
Executable file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: consts.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: consts
|
||||
"""
|
||||
|
||||
# SUPPORT PROJECT
|
||||
SUPPORT_PROJECT_BASIC = 'PROJECT_BASIC'
|
||||
SUPPORT_PROJECT_MEDICAL_METAVERSE = 'METAVERSE - MEDICAL'
|
||||
# SUPPORT_PROJECT_SW_LICENSE_MANAGER = 'SW LICENSE MANAGER'
|
||||
|
||||
PROJECT_NAME = 'METAVERSE - MEDICAL'
|
||||
SW_TITLE= f'{PROJECT_NAME} - REST API'
|
||||
SW_VERSION = '0.9'
|
||||
SW_DESCRIPTION = f'''
|
||||
### METAVERSE MEDICAL REST API
|
||||
|
||||
## API 이용법
|
||||
- **사용자 접속**시 토큰정보(***Authorization*** 항목) 획득 (이후 API 이용가능)
|
||||
- 수신받은 토큰정보(***Authorization*** 항목)를 ***Request Header*** 에 포함해서 기타 API 사용
|
||||
- 개별 API 설명과 Request/Response schema 참조
|
||||
|
||||
**시스템에서 사용하는 날짜형식**\n
|
||||
YYYY-mm-ddTHH:MM:SS
|
||||
YYYY-mm-dd HH:MM:SS
|
||||
'''
|
||||
TERMS_OF_SERVICE = 'http://www.a2tec.com'
|
||||
CONTEACT={
|
||||
'name': 'A2TEC (주)에이투텍',
|
||||
'url': 'http://www.a2tec.co.kr',
|
||||
'email': 'marketing@a2tec.co.kr'
|
||||
}
|
||||
LICENSE_INFO = {
|
||||
'name': 'Copyright by A2TEC', 'url': 'http://www.a2tec.co.kr'
|
||||
}
|
||||
|
||||
REST_SERVER_PORT = 50510
|
||||
|
||||
# ADMINISTRATOR ACCOUNT INFORMATION
|
||||
ADMIN_INIT_ACCOUNT_INFO = dict(
|
||||
id=1,
|
||||
account='a2tecrest01@gmail.com',
|
||||
pw='!dpdlxnxpr1',
|
||||
name='administrator',
|
||||
email='a2tecrest01@gmail.com',
|
||||
email_pw='!dpdlxnxpr1',
|
||||
)
|
||||
ADMIN_ACCOUNT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiYWNjb3VudCI6ImEydGVjcmVzdDAxQGdtYWlsLmNvbSIsIm5hbWUiOiJhZG1pbmlzdHJhdG9yIiwicGhvbmVfbnVtYmVyIjpudWxsLCJwaWN0dXJlIjpudWxsLCJhY2NvdW50X3R5cGUiOiJlbWFpbCJ9.QNiF4sc7BZYwzoir0oPqLCUIyglUgMjZuBc1mI2GRCE'
|
||||
|
||||
COOKIES_AUTH = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTQsImVtYWlsIjoia29hbGFAZGluZ3JyLmNvbSIsIm5hbWUiOm51bGwsInBob25lX251bWJlciI6bnVsbCwicHJvZmlsZV9pbWciOm51bGwsInNuc190eXBlIjpudWxsfQ.4vgrFvxgH8odoXMvV70BBqyqXOFa2NDQtzYkGywhV48'
|
||||
|
||||
JWT_SECRET = 'ABCD1234!'
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
EXCEPT_PATH_LIST = ['/', '/openapi.json']
|
||||
EXCEPT_PATH_REGEX = '^(/docs|/redoc|/api/auth' +\
|
||||
'|/api/user/check_account_exist' +\
|
||||
')'
|
||||
MAX_API_KEY = 3
|
||||
MAX_API_WHITELIST = 10
|
||||
|
||||
NUM_RETRY_UUID_GEN = 3
|
||||
|
||||
# DATABASE
|
||||
# DB_ADDRESS = '52.78.56.148'
|
||||
DB_ADDRESS = 'localhost'
|
||||
DB_USER_ID = 'medical_metaverse'
|
||||
DB_USER_PW = '1234'
|
||||
DB_NAME = 'medical_metaverse'
|
||||
DB_CHARSET = 'utf8mb4'
|
||||
|
||||
# Mail
|
||||
MAIL_REG_TITLE = f'{PROJECT_NAME} - Registration'
|
||||
MAIL_REG_CONTENTS = '''
|
||||
안녕하세요.
|
||||
[A2TEC] Metaverse medical Manager 입니다.
|
||||
|
||||
요청하신 라이선스가 정상 등록되었습니다.
|
||||
|
||||
라이선스 정보는 다음과 같습니다.
|
||||
Account: {}
|
||||
MAC: {}
|
||||
|
||||
|
||||
감사합니다.
|
||||
|
||||
'''
|
||||
|
||||
# MEETING_ROOM
|
||||
MEETING_ROOM_MAX_PERSON = 10
|
||||
144
fast_api/app/database/conn.py
Executable file
144
fast_api/app/database/conn.py
Executable file
@@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: conn.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: DB Connections
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import logging
|
||||
|
||||
|
||||
def _database_exist(engine, schema_name):
|
||||
query = f'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "{schema_name}"'
|
||||
with engine.connect() as conn:
|
||||
result_proxy = conn.execute(query)
|
||||
result = result_proxy.scalar()
|
||||
return bool(result)
|
||||
|
||||
|
||||
def _drop_database(engine, schema_name):
|
||||
with engine.connect() as conn:
|
||||
conn.execute(f'DROP DATABASE {schema_name};')
|
||||
|
||||
|
||||
def _create_database(engine, schema_name):
|
||||
with engine.connect() as conn:
|
||||
conn.execute(f'CREATE DATABASE {schema_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;')
|
||||
|
||||
|
||||
class SQLAlchemy:
|
||||
def __init__(self, app: FastAPI = None, **kwargs):
|
||||
self._engine = None
|
||||
self._session = None
|
||||
if app is not None:
|
||||
self.init_app(app=app, **kwargs)
|
||||
|
||||
def init_app(self, app: FastAPI, **kwargs):
|
||||
"""
|
||||
DB 초기화 함수
|
||||
:param app: FastAPI 인스턴스
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
database_url = kwargs.get('DB_URL')
|
||||
pool_recycle = kwargs.setdefault('DB_POOL_RECYCLE', 900)
|
||||
is_testing = kwargs.setdefault('TEST_MODE', False)
|
||||
echo = kwargs.setdefault('DB_ECHO', True)
|
||||
|
||||
self._engine = create_engine(
|
||||
database_url,
|
||||
echo=echo,
|
||||
pool_recycle=pool_recycle,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
if is_testing: # create schema
|
||||
db_url = self._engine.url
|
||||
if db_url.host != 'localhost':
|
||||
raise Exception('db host must be \'localhost\' in test environment')
|
||||
except_schema_db_url = f'{db_url.drivername}://{db_url.username}:{db_url.password}@{db_url.host}'
|
||||
schema_name = db_url.database
|
||||
temp_engine = create_engine(except_schema_db_url, echo=echo, pool_recycle=pool_recycle, pool_pre_ping=True)
|
||||
if _database_exist(temp_engine, schema_name):
|
||||
_drop_database(temp_engine, schema_name)
|
||||
_create_database(temp_engine, schema_name)
|
||||
temp_engine.dispose()
|
||||
else:
|
||||
db_url = self._engine.url
|
||||
except_schema_db_url = f'{db_url.drivername}://{db_url.username}:{db_url.password}@{db_url.host}'
|
||||
schema_name = db_url.database
|
||||
temp_engine = create_engine(except_schema_db_url, echo=echo, pool_recycle=pool_recycle, pool_pre_ping=True)
|
||||
if not _database_exist(temp_engine, schema_name):
|
||||
_create_database(temp_engine, schema_name)
|
||||
Base.metadata.create_all(db.engine)
|
||||
temp_engine.dispose()
|
||||
|
||||
self._session = sessionmaker(autocommit=False, autoflush=False, bind=self._engine)
|
||||
|
||||
# NOTE(hsj100): ADMINISTRATOR
|
||||
create_admin(self._session)
|
||||
|
||||
@app.on_event('startup')
|
||||
def startup():
|
||||
self._engine.connect()
|
||||
logging.info('DB connected.')
|
||||
|
||||
@app.on_event('shutdown')
|
||||
def shutdown():
|
||||
self._session.close_all()
|
||||
self._engine.dispose()
|
||||
logging.info('DB disconnected')
|
||||
|
||||
def get_db(self):
|
||||
"""
|
||||
요청마다 DB 세션 유지 함수
|
||||
:return:
|
||||
"""
|
||||
if self._session is None:
|
||||
raise Exception('must be called \'init_app\'')
|
||||
db_session = None
|
||||
try:
|
||||
db_session = self._session()
|
||||
yield db_session
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self.get_db
|
||||
|
||||
@property
|
||||
def engine(self):
|
||||
return self._engine
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# NOTE(hsj100): ADMINISTRATOR
|
||||
def create_admin(db_session):
|
||||
import bcrypt
|
||||
from app.database.schema import Users
|
||||
from app.common.consts import ADMIN_INIT_ACCOUNT_INFO
|
||||
|
||||
session = db_session()
|
||||
|
||||
if not session:
|
||||
raise Exception('cat`t create account of admin')
|
||||
|
||||
if Users.get(account=ADMIN_INIT_ACCOUNT_INFO['account']):
|
||||
return
|
||||
|
||||
admin = {**ADMIN_INIT_ACCOUNT_INFO}
|
||||
hash_pw = bcrypt.hashpw(str(admin['pw']).encode('utf-8'), bcrypt.gensalt())
|
||||
admin['pw'] = hash_pw
|
||||
|
||||
Users.create(session=session, auto_commit=True, **admin)
|
||||
149
fast_api/app/database/crud.py
Executable file
149
fast_api/app/database/crud.py
Executable file
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: crud.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: CRUD
|
||||
"""
|
||||
|
||||
|
||||
async def table_read(accessor_info, target_table, request_body_info, response_model, response_model_data):
|
||||
"""
|
||||
table_read
|
||||
"""
|
||||
try:
|
||||
# parameter
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
|
||||
if not target_table:
|
||||
raise Exception(f'invalid table_name:{target_table}')
|
||||
|
||||
# if not request_body_info:
|
||||
# raise Exception('invalid request_body_info')
|
||||
|
||||
if not response_model:
|
||||
raise Exception('invalid response_model')
|
||||
|
||||
if not response_model_data:
|
||||
raise Exception('invalid response_model_data')
|
||||
|
||||
# request
|
||||
request_info = dict()
|
||||
for key, val in request_body_info.dict().items():
|
||||
if val:
|
||||
request_info[key] = val
|
||||
|
||||
# search
|
||||
search_info = target_table.filter(**request_info).all()
|
||||
if not search_info:
|
||||
raise Exception('not found data')
|
||||
|
||||
# result
|
||||
result_info = list()
|
||||
for value in search_info:
|
||||
result_info.append(response_model_data.from_orm(value))
|
||||
return response_model(data=result_info)
|
||||
except Exception as e:
|
||||
return response_model.set_error(str(e))
|
||||
|
||||
|
||||
async def table_update(accessor_info, target_table, request_body_info, response_model, response_model_data=None):
|
||||
search_info = None
|
||||
|
||||
try:
|
||||
|
||||
# parameter
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
|
||||
if not target_table:
|
||||
raise Exception(f'invalid table_name:{target_table}')
|
||||
|
||||
if not request_body_info:
|
||||
raise Exception('invalid request_body_info')
|
||||
|
||||
if not response_model:
|
||||
raise Exception('invalid response_model')
|
||||
|
||||
# if not response_model_data:
|
||||
# raise Exception('invalid response_model_data')
|
||||
|
||||
# request
|
||||
if not request_body_info.search_info:
|
||||
raise Exception('invalid request_body: search_info')
|
||||
request_search_info = dict()
|
||||
if request_body_info.search_info:
|
||||
for key, val in request_body_info.search_info.dict().items():
|
||||
if val:
|
||||
request_search_info[key] = val
|
||||
if not request_search_info:
|
||||
raise Exception('invalid request_body: search_info')
|
||||
|
||||
if not request_body_info.update_info:
|
||||
raise Exception('invalid request_body: update_info')
|
||||
request_update_info = dict()
|
||||
for key, val in request_body_info.update_info.dict().items():
|
||||
if val:
|
||||
request_update_info[key] = val
|
||||
if not request_update_info:
|
||||
raise Exception('invalid request_body: update_info')
|
||||
|
||||
# search
|
||||
search_info = target_table.filter(**request_search_info)
|
||||
|
||||
# process
|
||||
search_info.update(auto_commit=True, synchronize_session=False, **request_update_info)
|
||||
|
||||
# result
|
||||
return response_model()
|
||||
except Exception as e:
|
||||
if search_info:
|
||||
search_info.close()
|
||||
return response_model.set_error(str(e))
|
||||
|
||||
|
||||
async def table_delete(accessor_info, target_table, request_body_info, response_model, response_model_data=None):
|
||||
search_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
|
||||
if not target_table:
|
||||
raise Exception(f'invalid table_name:{target_table}')
|
||||
|
||||
if not request_body_info:
|
||||
raise Exception('invalid request_body_info')
|
||||
|
||||
if not response_model:
|
||||
raise Exception('invalid response_model')
|
||||
|
||||
# if not response_model_data:
|
||||
# raise Exception('invalid response_model_data')
|
||||
|
||||
# request
|
||||
request_search_info = dict()
|
||||
if request_body_info:
|
||||
for key, val in request_body_info.dict().items():
|
||||
if val:
|
||||
request_search_info[key] = val
|
||||
if not request_search_info:
|
||||
raise Exception('invalid request_body')
|
||||
|
||||
# search
|
||||
search_info = target_table.filter(**request_search_info)
|
||||
|
||||
# process
|
||||
search_info.delete(auto_commit=True, synchronize_session=False)
|
||||
|
||||
# result
|
||||
return response_model()
|
||||
except Exception as e:
|
||||
if search_info:
|
||||
search_info.close()
|
||||
return response_model.set_error(str(e))
|
||||
272
fast_api/app/database/schema.py
Executable file
272
fast_api/app/database/schema.py
Executable file
@@ -0,0 +1,272 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: schema.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: database schema
|
||||
"""
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
func,
|
||||
Enum,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import Session, relationship
|
||||
|
||||
from app.database.conn import Base, db
|
||||
from app.utils.date_utils import D
|
||||
from app.models import (
|
||||
SexType,
|
||||
UserType,
|
||||
AccountType,
|
||||
UserStatusType,
|
||||
UserLoginType,
|
||||
RoomType,
|
||||
AppointmentStatusType,
|
||||
RoomStatusType
|
||||
)
|
||||
from app.common.consts import MEETING_ROOM_MAX_PERSON
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, nullable=False, default=D.datetime())
|
||||
updated_at = Column(DateTime, nullable=False, default=D.datetime(), onupdate=D.datetime())
|
||||
|
||||
def __init__(self):
|
||||
self._q = None
|
||||
self._session = None
|
||||
self.served = None
|
||||
|
||||
def all_columns(self):
|
||||
return [c for c in self.__table__.columns if c.primary_key is False and c.name != 'created_at']
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
@classmethod
|
||||
def create(cls, session: Session, auto_commit=False, **kwargs):
|
||||
"""
|
||||
테이블 데이터 적재 전용 함수
|
||||
:param session:
|
||||
:param auto_commit: 자동 커밋 여부
|
||||
:param kwargs: 적재 할 데이터
|
||||
:return:
|
||||
"""
|
||||
obj = cls()
|
||||
|
||||
# NOTE(hsj100) : FIX_DATETIME
|
||||
if 'created_at' not in kwargs:
|
||||
obj.created_at = D.datetime()
|
||||
if 'updated_at' not in kwargs:
|
||||
obj.updated_at = D.datetime()
|
||||
|
||||
for col in obj.all_columns():
|
||||
col_name = col.name
|
||||
if col_name in kwargs:
|
||||
setattr(obj, col_name, kwargs.get(col_name))
|
||||
session.add(obj)
|
||||
session.flush()
|
||||
if auto_commit:
|
||||
session.commit()
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get(cls, session: Session = None, **kwargs):
|
||||
"""
|
||||
Simply get a Row
|
||||
:param session:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
sess = next(db.session()) if not session else session
|
||||
query = sess.query(cls)
|
||||
for key, val in kwargs.items():
|
||||
col = getattr(cls, key)
|
||||
query = query.filter(col == val)
|
||||
|
||||
if query.count() > 1:
|
||||
raise Exception('Only one row is supposed to be returned, but got more than one.')
|
||||
result = query.first()
|
||||
if not session:
|
||||
sess.close()
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def filter(cls, session: Session = None, **kwargs):
|
||||
"""
|
||||
Simply get a Row
|
||||
:param session:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
cond = []
|
||||
for key, val in kwargs.items():
|
||||
key = key.split('__')
|
||||
if len(key) > 2:
|
||||
raise Exception('No 2 more dunders')
|
||||
col = getattr(cls, key[0])
|
||||
if len(key) == 1: cond.append((col == val))
|
||||
elif len(key) == 2 and key[1] == 'gt': cond.append((col > val))
|
||||
elif len(key) == 2 and key[1] == 'gte': cond.append((col >= val))
|
||||
elif len(key) == 2 and key[1] == 'lt': cond.append((col < val))
|
||||
elif len(key) == 2 and key[1] == 'lte': cond.append((col <= val))
|
||||
elif len(key) == 2 and key[1] == 'in': cond.append((col.in_(val)))
|
||||
elif len(key) == 2 and key[1] == 'like': cond.append((col.like(val)))
|
||||
|
||||
obj = cls()
|
||||
if session:
|
||||
obj._session = session
|
||||
obj.served = True
|
||||
else:
|
||||
obj._session = next(db.session())
|
||||
obj.served = False
|
||||
query = obj._session.query(cls)
|
||||
query = query.filter(*cond)
|
||||
obj._q = query
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def cls_attr(cls, col_name=None):
|
||||
if col_name:
|
||||
col = getattr(cls, col_name)
|
||||
return col
|
||||
else:
|
||||
return cls
|
||||
|
||||
def order_by(self, *args: str):
|
||||
for a in args:
|
||||
if a.startswith('-'):
|
||||
col_name = a[1:]
|
||||
is_asc = False
|
||||
else:
|
||||
col_name = a
|
||||
is_asc = True
|
||||
col = self.cls_attr(col_name)
|
||||
self._q = self._q.order_by(col.asc()) if is_asc else self._q.order_by(col.desc())
|
||||
return self
|
||||
|
||||
def update(self, auto_commit: bool = False, synchronize_session='evaluate', **kwargs):
|
||||
# NOTE(hsj100) : FIX_DATETIME
|
||||
if 'updated_at' not in kwargs:
|
||||
kwargs['updated_at'] = D.datetime()
|
||||
|
||||
qs = self._q.update(kwargs, synchronize_session=synchronize_session)
|
||||
get_id = self.id
|
||||
ret = None
|
||||
|
||||
self._session.flush()
|
||||
if qs > 0 :
|
||||
ret = self._q.first()
|
||||
if auto_commit:
|
||||
self._session.commit()
|
||||
return ret
|
||||
|
||||
def first(self):
|
||||
result = self._q.first()
|
||||
self.close()
|
||||
return result
|
||||
|
||||
def delete(self, auto_commit: bool = False, synchronize_session='evaluate'):
|
||||
self._q.delete(synchronize_session=synchronize_session)
|
||||
if auto_commit:
|
||||
self._session.commit()
|
||||
|
||||
def all(self):
|
||||
print(self.served)
|
||||
result = self._q.all()
|
||||
self.close()
|
||||
return result
|
||||
|
||||
def count(self):
|
||||
result = self._q.count()
|
||||
self.close()
|
||||
return result
|
||||
|
||||
def close(self):
|
||||
if not self.served:
|
||||
self._session.close()
|
||||
else:
|
||||
self._session.flush()
|
||||
|
||||
|
||||
class ApiKeys(Base, BaseMixin):
|
||||
__tablename__ = 'api_keys'
|
||||
access_key = Column(String(length=64), nullable=False, index=True)
|
||||
secret_key = Column(String(length=64), nullable=False)
|
||||
user_memo = Column(String(length=40), nullable=True)
|
||||
status = Column(Enum('active', 'stopped', 'deleted'), default='active')
|
||||
is_whitelisted = Column(Boolean, default=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
whitelist = relationship('ApiWhiteLists', backref='api_keys')
|
||||
users = relationship('Users', back_populates='keys')
|
||||
|
||||
|
||||
class ApiWhiteLists(Base, BaseMixin):
|
||||
__tablename__ = 'api_whitelists'
|
||||
ip_addr = Column(String(length=64), nullable=False)
|
||||
api_key_id = Column(Integer, ForeignKey('api_keys.id'), nullable=False)
|
||||
|
||||
|
||||
class Users(Base, BaseMixin):
|
||||
__tablename__ = 'users'
|
||||
status = Column(Enum(UserStatusType), nullable=False, default=UserStatusType.active)
|
||||
# TODO(hsj100): LOGIN_STATUS
|
||||
login = Column(Enum(UserLoginType), nullable=False, default=UserLoginType.logout)
|
||||
account_type = Column(Enum(AccountType), nullable=False, default=AccountType.email)
|
||||
account = Column(String(length=255), nullable=False, unique=True)
|
||||
pw = Column(String(length=2000), nullable=True)
|
||||
email = Column(String(length=255), nullable=True)
|
||||
name = Column(String(length=255), nullable=False)
|
||||
sex = Column(Enum(SexType), nullable=False, default=SexType.male)
|
||||
rrn = Column(String(length=255), nullable=True)
|
||||
address = Column(String(length=2000), nullable=True)
|
||||
phone_number = Column(String(length=20), nullable=True)
|
||||
picture = Column(String(length=1000), nullable=True)
|
||||
marketing_agree = Column(Boolean, nullable=True, default=False)
|
||||
|
||||
keys = relationship('ApiKeys', back_populates='users')
|
||||
|
||||
user_type = Column(Enum(UserType), nullable=False, default=UserType.patient)
|
||||
major = Column(String(length=255), nullable=True, default=None)
|
||||
character = Column(String(length=255), nullable=True)
|
||||
|
||||
room_id = Column(Integer, ForeignKey('room.id'), nullable=True)
|
||||
|
||||
|
||||
class Appointment(Base, BaseMixin):
|
||||
__tablename__ = 'appointment'
|
||||
create_account = Column(String(length=255), nullable=False)
|
||||
doctor_account = Column(String(length=255), nullable=False)
|
||||
patient_account = Column(String(length=255), nullable=False)
|
||||
treatment_subject = Column(String(length=64), nullable=False)
|
||||
date = Column(DateTime, nullable=False, default=func.timestamp())
|
||||
status = Column(Enum(AppointmentStatusType), default=AppointmentStatusType.wait)
|
||||
|
||||
|
||||
class TreatmentDivision(Base, BaseMixin):
|
||||
__tablename__ = 'treat'
|
||||
short_name = Column(String(length=64), nullable=False)
|
||||
full_name = Column(String(length=255), nullable=False)
|
||||
|
||||
|
||||
class Room(Base, BaseMixin):
|
||||
__tablename__ = 'room'
|
||||
type = Column(Enum(RoomType), nullable=False, default=RoomType.meeting)
|
||||
auto_enter = Column(Boolean, nullable=False, default=True)
|
||||
auto_delete = Column(Boolean, nullable=False, default=True)
|
||||
name = Column(String(length=255), nullable=False)
|
||||
pw = Column(String(length=2000), nullable=False)
|
||||
create_account = Column(String(length=255), nullable=False)
|
||||
status = Column(Enum(RoomStatusType), nullable=False, default=RoomStatusType.open)
|
||||
cur_person = Column(Integer, nullable=False, default=0)
|
||||
max_person = Column(Integer, nullable=False, default=MEETING_ROOM_MAX_PERSON)
|
||||
desc = Column(String(length=2000), nullable=True)
|
||||
0
fast_api/app/errors/__init__.py
Executable file
0
fast_api/app/errors/__init__.py
Executable file
188
fast_api/app/errors/exceptions.py
Executable file
188
fast_api/app/errors/exceptions.py
Executable file
@@ -0,0 +1,188 @@
|
||||
from app.common.consts import MAX_API_KEY, MAX_API_WHITELIST
|
||||
|
||||
|
||||
class StatusCode:
|
||||
HTTP_500 = 500
|
||||
HTTP_400 = 400
|
||||
HTTP_401 = 401
|
||||
HTTP_403 = 403
|
||||
HTTP_404 = 404
|
||||
HTTP_405 = 405
|
||||
|
||||
|
||||
class APIException(Exception):
|
||||
status_code: int
|
||||
code: str
|
||||
msg: str
|
||||
detail: str
|
||||
ex: Exception
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
status_code: int = StatusCode.HTTP_500,
|
||||
code: str = '000000',
|
||||
msg: str = None,
|
||||
detail: str = None,
|
||||
ex: Exception = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
self.detail = detail
|
||||
self.ex = ex
|
||||
super().__init__(ex)
|
||||
|
||||
|
||||
class NotFoundUserEx(APIException):
|
||||
def __init__(self, user_id: int = None, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_404,
|
||||
msg=f'해당 유저를 찾을 수 없습니다.',
|
||||
detail=f'Not Found User ID : {user_id}',
|
||||
code=f'{StatusCode.HTTP_400}{"1".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class NotAuthorized(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_401,
|
||||
msg=f'로그인이 필요한 서비스 입니다.',
|
||||
detail='Authorization Required',
|
||||
code=f'{StatusCode.HTTP_401}{"1".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class TokenExpiredEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'세션이 만료되어 로그아웃 되었습니다.',
|
||||
detail='Token Expired',
|
||||
code=f'{StatusCode.HTTP_400}{"1".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class TokenDecodeEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'비정상적인 접근입니다.',
|
||||
detail='Token has been compromised.',
|
||||
code=f'{StatusCode.HTTP_400}{"2".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class NoKeyMatchEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_404,
|
||||
msg=f'해당 키에 대한 권한이 없거나 해당 키가 없습니다.',
|
||||
detail='No Keys Matched',
|
||||
code=f'{StatusCode.HTTP_404}{"3".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class MaxKeyCountEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'API 키 생성은 {MAX_API_KEY}개 까지 가능합니다.',
|
||||
detail='Max Key Count Reached',
|
||||
code=f'{StatusCode.HTTP_400}{"4".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class MaxWLCountEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'화이트리스트 생성은 {MAX_API_WHITELIST}개 까지 가능합니다.',
|
||||
detail='Max Whitelist Count Reached',
|
||||
code=f'{StatusCode.HTTP_400}{"5".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class InvalidIpEx(APIException):
|
||||
def __init__(self, ip: str, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'{ip}는 올바른 IP 가 아닙니다.',
|
||||
detail=f'invalid IP : {ip}',
|
||||
code=f'{StatusCode.HTTP_400}{"6".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class SqlFailureEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_500,
|
||||
msg=f'이 에러는 서버측 에러 입니다. 자동으로 리포팅 되며, 빠르게 수정하겠습니다.',
|
||||
detail='Internal Server Error',
|
||||
code=f'{StatusCode.HTTP_500}{"2".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class APIQueryStringEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'쿼리스트링은 key, timestamp 2개만 허용되며, 2개 모두 요청시 제출되어야 합니다.',
|
||||
detail='Query String Only Accept key and timestamp.',
|
||||
code=f'{StatusCode.HTTP_400}{"7".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class APIHeaderInvalidEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'헤더에 키 해싱된 Secret 이 없거나, 유효하지 않습니다.',
|
||||
detail='Invalid HMAC secret in Header',
|
||||
code=f'{StatusCode.HTTP_400}{"8".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class APITimestampEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'쿼리스트링에 포함된 타임스탬프는 KST 이며, 현재 시간보다 작아야 하고, 현재시간 - 10초 보다는 커야 합니다.',
|
||||
detail='timestamp in Query String must be KST, Timestamp must be less than now, and greater than now - 10.',
|
||||
code=f'{StatusCode.HTTP_400}{"9".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class NotFoundAccessKeyEx(APIException):
|
||||
def __init__(self, api_key: str, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_404,
|
||||
msg=f'API 키를 찾을 수 없습니다.',
|
||||
detail=f'Not found such API Access Key : {api_key}',
|
||||
code=f'{StatusCode.HTTP_404}{"10".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
|
||||
|
||||
class KakaoSendFailureEx(APIException):
|
||||
def __init__(self, ex: Exception = None):
|
||||
super().__init__(
|
||||
status_code=StatusCode.HTTP_400,
|
||||
msg=f'카카오톡 전송에 실패했습니다.',
|
||||
detail=f'Failed to send KAKAO MSG.',
|
||||
code=f'{StatusCode.HTTP_400}{"11".zfill(4)}',
|
||||
ex=ex,
|
||||
)
|
||||
84
fast_api/app/main.py
Executable file
84
fast_api/app/main.py
Executable file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: main.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: Main
|
||||
"""
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi.security import APIKeyHeader
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.common import consts
|
||||
|
||||
from app.database.conn import db
|
||||
from app.common.config import conf
|
||||
from app.middlewares.token_validator import access_control
|
||||
from app.middlewares.trusted_hosts import TrustedHostMiddleware
|
||||
from app.routes import dev, index, auth, users, services
|
||||
|
||||
|
||||
API_KEY_HEADER = APIKeyHeader(name='Authorization', auto_error=False)
|
||||
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
fast_api_app 생성
|
||||
|
||||
:return: fast_api_app
|
||||
"""
|
||||
configurations = conf()
|
||||
fast_api_app = FastAPI(
|
||||
title=configurations.SW_TITLE,
|
||||
version=configurations.SW_VERSION,
|
||||
description=configurations.SW_DESCRIPTION,
|
||||
terms_of_service=configurations.TERMS_OF_SERVICE,
|
||||
contact=configurations.CONTEACT,
|
||||
license_info={'name': 'Copyright by A2TEC', 'url': 'http://www.a2tec.co.kr'}
|
||||
)
|
||||
|
||||
# 데이터 베이스 이니셜라이즈
|
||||
conf_dict = asdict(configurations)
|
||||
db.init_app(fast_api_app, **conf_dict)
|
||||
|
||||
# 레디스 이니셜라이즈
|
||||
|
||||
# 미들웨어 정의
|
||||
fast_api_app.add_middleware(middleware_class=BaseHTTPMiddleware, dispatch=access_control)
|
||||
fast_api_app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=conf().ALLOW_SITE,
|
||||
allow_credentials=True,
|
||||
allow_methods=['*'],
|
||||
allow_headers=['*'],
|
||||
)
|
||||
fast_api_app.add_middleware(TrustedHostMiddleware, allowed_hosts=conf().TRUSTED_HOSTS, except_path=['/health'])
|
||||
|
||||
# 라우터 정의
|
||||
fast_api_app.include_router(index.router, tags=['Defaults'])
|
||||
fast_api_app.include_router(auth.router, tags=['Authentication'], prefix='/api')
|
||||
|
||||
if conf().DEBUG:
|
||||
fast_api_app.include_router(services.router, tags=['Services'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
|
||||
else:
|
||||
fast_api_app.include_router(services.router, tags=['Services'], prefix='/api')
|
||||
|
||||
fast_api_app.include_router(users.router, tags=['Users'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
|
||||
|
||||
if conf().DEBUG:
|
||||
fast_api_app.include_router(dev.router, tags=['Developments'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
|
||||
return fast_api_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run('main:app', host='0.0.0.0', port=conf().REST_SERVER_PORT, reload=True)
|
||||
166
fast_api/app/middlewares/token_validator.py
Executable file
166
fast_api/app/middlewares/token_validator.py
Executable file
@@ -0,0 +1,166 @@
|
||||
import base64
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
import typing
|
||||
import re
|
||||
|
||||
import jwt
|
||||
import sqlalchemy.exc
|
||||
|
||||
from jwt.exceptions import ExpiredSignatureError, DecodeError
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from app.common.consts import EXCEPT_PATH_LIST, EXCEPT_PATH_REGEX
|
||||
from app.database.conn import db
|
||||
from app.database.schema import Users, ApiKeys
|
||||
from app.errors import exceptions as ex
|
||||
|
||||
from app.common import consts
|
||||
from app.common.config import conf
|
||||
from app.errors.exceptions import APIException, SqlFailureEx, APIQueryStringEx
|
||||
from app.models import UserToken
|
||||
|
||||
from app.utils.date_utils import D
|
||||
from app.utils.logger import api_logger
|
||||
from app.utils.query_utils import to_dict
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
|
||||
async def access_control(request: Request, call_next):
|
||||
request.state.req_time = D.datetime()
|
||||
request.state.start = time.time()
|
||||
request.state.inspect = None
|
||||
request.state.user = None
|
||||
request.state.service = None
|
||||
|
||||
ip = request.headers['x-forwarded-for'] if 'x-forwarded-for' in request.headers.keys() else request.client.host
|
||||
request.state.ip = ip.split(',')[0] if ',' in ip else ip
|
||||
headers = request.headers
|
||||
cookies = request.cookies
|
||||
|
||||
url = request.url.path
|
||||
if await url_pattern_check(url, EXCEPT_PATH_REGEX) or url in EXCEPT_PATH_LIST:
|
||||
response = await call_next(request)
|
||||
if url != '/':
|
||||
await api_logger(request=request, response=response)
|
||||
return response
|
||||
|
||||
try:
|
||||
if url.startswith('/api'):
|
||||
# api 인경우 헤더로 토큰 검사
|
||||
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
|
||||
if url.startswith('/api/services') and conf().SERVICE_AUTH_API_KEY:
|
||||
qs = str(request.query_params)
|
||||
qs_list = qs.split('&')
|
||||
session = next(db.session())
|
||||
if not conf().DEBUG:
|
||||
try:
|
||||
qs_dict = {qs_split.split('=')[0]: qs_split.split('=')[1] for qs_split in qs_list}
|
||||
except Exception:
|
||||
raise ex.APIQueryStringEx()
|
||||
|
||||
qs_keys = qs_dict.keys()
|
||||
|
||||
if 'key' not in qs_keys or 'timestamp' not in qs_keys:
|
||||
raise ex.APIQueryStringEx()
|
||||
|
||||
if 'secret' not in headers.keys():
|
||||
raise ex.APIHeaderInvalidEx()
|
||||
|
||||
api_key = ApiKeys.get(session=session, access_key=qs_dict['key'])
|
||||
|
||||
if not api_key:
|
||||
raise ex.NotFoundAccessKeyEx(api_key=qs_dict['key'])
|
||||
mac = hmac.new(bytes(api_key.secret_key, encoding='utf8'), bytes(qs, encoding='utf-8'), digestmod='sha256')
|
||||
d = mac.digest()
|
||||
validating_secret = str(base64.b64encode(d).decode('utf-8'))
|
||||
|
||||
if headers['secret'] != validating_secret:
|
||||
raise ex.APIHeaderInvalidEx()
|
||||
|
||||
now_timestamp = int(D.datetime(diff=9).timestamp())
|
||||
if now_timestamp - 10 > int(qs_dict['timestamp']) or now_timestamp < int(qs_dict['timestamp']):
|
||||
raise ex.APITimestampEx()
|
||||
|
||||
user_info = to_dict(api_key.users)
|
||||
request.state.user = UserToken(**user_info)
|
||||
|
||||
else:
|
||||
# Request User 가 필요함
|
||||
if 'authorization' in headers.keys():
|
||||
key = headers.get('Authorization')
|
||||
api_key_obj = ApiKeys.get(session=session, access_key=key)
|
||||
user_info = to_dict(Users.get(session=session, id=api_key_obj.user_id))
|
||||
request.state.user = UserToken(**user_info)
|
||||
# 토큰 없음
|
||||
else:
|
||||
if 'Authorization' not in headers.keys():
|
||||
raise ex.NotAuthorized()
|
||||
session.close()
|
||||
response = await call_next(request)
|
||||
return response
|
||||
else:
|
||||
if 'authorization' in headers.keys():
|
||||
# 토큰 존재
|
||||
token_info = await token_decode(access_token=headers.get('Authorization'))
|
||||
request.state.user = UserToken(**token_info)
|
||||
elif conf().DEV_TEST_CONNECT_ACCOUNT:
|
||||
# NOTE(hsj100): DEV_TEST_CONNECT_ACCOUNT
|
||||
request.state.user = UserToken.from_orm(conf().DEV_TEST_CONNECT_ACCOUNT)
|
||||
else:
|
||||
# 토큰 없음
|
||||
if 'Authorization' not in headers.keys():
|
||||
raise ex.NotAuthorized()
|
||||
else:
|
||||
# 템플릿 렌더링인 경우 쿠키에서 토큰 검사
|
||||
cookies['Authorization'] = conf().COOKIES_AUTH
|
||||
|
||||
if 'Authorization' not in cookies.keys():
|
||||
raise ex.NotAuthorized()
|
||||
|
||||
token_info = await token_decode(access_token=cookies.get('Authorization'))
|
||||
request.state.user = UserToken(**token_info)
|
||||
response = await call_next(request)
|
||||
await api_logger(request=request, response=response)
|
||||
except Exception as e:
|
||||
|
||||
error = await exception_handler(e)
|
||||
error_dict = dict(status=error.status_code, msg=error.msg, detail=error.detail, code=error.code)
|
||||
response = JSONResponse(status_code=error.status_code, content=error_dict)
|
||||
await api_logger(request=request, error=error)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def url_pattern_check(path, pattern):
|
||||
result = re.match(pattern, path)
|
||||
if result:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def token_decode(access_token):
|
||||
"""
|
||||
:param access_token:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
access_token = access_token.replace('Bearer ', "")
|
||||
payload = jwt.decode(access_token, key=consts.JWT_SECRET, algorithms=[consts.JWT_ALGORITHM])
|
||||
except ExpiredSignatureError:
|
||||
raise ex.TokenExpiredEx()
|
||||
except DecodeError:
|
||||
raise ex.TokenDecodeEx()
|
||||
return payload
|
||||
|
||||
|
||||
async def exception_handler(error: Exception):
|
||||
print(error)
|
||||
if isinstance(error, sqlalchemy.exc.OperationalError):
|
||||
error = SqlFailureEx(ex=error)
|
||||
if not isinstance(error, APIException):
|
||||
error = APIException(ex=error, detail=str(error))
|
||||
return error
|
||||
63
fast_api/app/middlewares/trusted_hosts.py
Executable file
63
fast_api/app/middlewares/trusted_hosts.py
Executable file
@@ -0,0 +1,63 @@
|
||||
import typing
|
||||
|
||||
|
||||
from starlette.datastructures import URL, Headers
|
||||
from starlette.responses import PlainTextResponse, RedirectResponse, Response
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
ENFORCE_DOMAIN_WILDCARD = 'Domain wildcard patterns must be lNo module named \'app\'ike \'*.example.com\'.'
|
||||
|
||||
|
||||
class TrustedHostMiddleware:
|
||||
def __init__(
|
||||
self,
|
||||
app: ASGIApp,
|
||||
allowed_hosts: typing.Sequence[str] = None,
|
||||
except_path: typing.Sequence[str] = None,
|
||||
www_redirect: bool = True,
|
||||
) -> None:
|
||||
if allowed_hosts is None:
|
||||
allowed_hosts = ['*']
|
||||
if except_path is None:
|
||||
except_path = []
|
||||
for pattern in allowed_hosts:
|
||||
assert '*' not in pattern[1:], ENFORCE_DOMAIN_WILDCARD
|
||||
if pattern.startswith('*') and pattern != '*':
|
||||
assert pattern.startswith('*.'), ENFORCE_DOMAIN_WILDCARD
|
||||
self.app = app
|
||||
self.allowed_hosts = list(allowed_hosts)
|
||||
self.allow_any = '*' in allowed_hosts
|
||||
self.www_redirect = www_redirect
|
||||
self.except_path = list(except_path)
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if self.allow_any or scope['type'] not in ('http', 'websocket',): # pragma: no cover
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
headers = Headers(scope=scope)
|
||||
host = headers.get('host', "").split(':')[0]
|
||||
is_valid_host = False
|
||||
found_www_redirect = False
|
||||
for pattern in self.allowed_hosts:
|
||||
if (
|
||||
host == pattern
|
||||
or (pattern.startswith('*') and host.endswith(pattern[1:]))
|
||||
or URL(scope=scope).path in self.except_path
|
||||
):
|
||||
is_valid_host = True
|
||||
break
|
||||
elif 'www.' + host == pattern:
|
||||
found_www_redirect = True
|
||||
|
||||
if is_valid_host:
|
||||
await self.app(scope, receive, send)
|
||||
else:
|
||||
if found_www_redirect and self.www_redirect:
|
||||
url = URL(scope=scope)
|
||||
redirect_url = url.replace(netloc='www.' + url.netloc)
|
||||
response = RedirectResponse(url=str(redirect_url)) # type: Response
|
||||
else:
|
||||
response = PlainTextResponse('Invalid host header', status_code=400)
|
||||
|
||||
await response(scope, receive, send)
|
||||
590
fast_api/app/models.py
Executable file
590
fast_api/app/models.py
Executable file
@@ -0,0 +1,590 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: models.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: data models
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.main import BaseModel
|
||||
from pydantic.networks import EmailStr, IPvAnyAddress
|
||||
from typing import Optional
|
||||
|
||||
from app.common.consts import SW_TITLE, SW_VERSION, MEETING_ROOM_MAX_PERSON
|
||||
from app.utils.date_utils import D
|
||||
|
||||
|
||||
class SWInfo(BaseModel):
|
||||
"""
|
||||
### 서비스 정보
|
||||
"""
|
||||
name: str = Field(SW_TITLE, description='SW 이름', example=SW_VERSION)
|
||||
version: str = Field(SW_VERSION, description='SW 버전', example=SW_VERSION)
|
||||
date: str = Field(D.date_str(), description='현재날짜', example='%Y.%m.%dT%H:%M:%S')
|
||||
|
||||
|
||||
class CustomEnum(Enum):
|
||||
@classmethod
|
||||
def get_elements_str(cls, is_key=True):
|
||||
if is_key:
|
||||
result_str = cls.__members__.keys()
|
||||
else:
|
||||
result_str = cls.__members__.values()
|
||||
return '[' + ', '.join(result_str) + ']'
|
||||
|
||||
|
||||
class SexType(str, CustomEnum):
|
||||
male: str = 'male'
|
||||
female: str = 'female'
|
||||
|
||||
|
||||
class AccountType(str, CustomEnum):
|
||||
email: str = 'email'
|
||||
# facebook: str = 'facebook'
|
||||
# google: str = 'google'
|
||||
# kakao: str = 'kakao'
|
||||
|
||||
|
||||
class UserStatusType(str, CustomEnum):
|
||||
active: str = 'active'
|
||||
deleted: str = 'deleted'
|
||||
blocked: str = 'blocked'
|
||||
|
||||
|
||||
class UserLoginType(str, CustomEnum):
|
||||
login: str = 'login'
|
||||
logout: str = 'logout'
|
||||
|
||||
class UserType(str, CustomEnum):
|
||||
doctor: str = 'doctor'
|
||||
patient: str = 'patient'
|
||||
|
||||
|
||||
class RoomType(str, CustomEnum):
|
||||
meeting: str = 'meeting'
|
||||
lecture: str = 'lecture'
|
||||
|
||||
|
||||
class AppointmentStatusType(str, CustomEnum):
|
||||
wait: str = 'wait'
|
||||
reserve: str = 'reserve'
|
||||
cancel: str = 'cancel'
|
||||
treatment: str = 'treatment'
|
||||
|
||||
|
||||
class RoomStatusType(str, CustomEnum):
|
||||
open: str = 'open'
|
||||
close: str = 'close'
|
||||
tclose: str = 'tclose'
|
||||
full: str = 'full'
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
Authorization: str = Field(None, description='인증키', example='Bearer [token]')
|
||||
|
||||
|
||||
class EmailRecipients(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
|
||||
|
||||
class SendEmail(BaseModel):
|
||||
email_to: List[EmailRecipients] = None
|
||||
|
||||
|
||||
class KakaoMsgBody(BaseModel):
|
||||
msg: str = None
|
||||
|
||||
|
||||
class MessageOk(BaseModel):
|
||||
message: str = Field(default='OK')
|
||||
|
||||
|
||||
class UserToken(BaseModel):
|
||||
id: int
|
||||
account: str = None
|
||||
name: str = None
|
||||
phone_number: str = None
|
||||
picture: str = None
|
||||
account_type: str = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AddApiKey(BaseModel):
|
||||
user_memo: str = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class GetApiKeyList(AddApiKey):
|
||||
id: int = None
|
||||
access_key: str = None
|
||||
created_at: datetime = None
|
||||
|
||||
|
||||
class GetApiKeys(GetApiKeyList):
|
||||
secret_key: str = None
|
||||
|
||||
|
||||
class CreateAPIWhiteLists(BaseModel):
|
||||
ip_addr: str = None
|
||||
|
||||
|
||||
class GetAPIWhiteLists(CreateAPIWhiteLists):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class ResponseBase(BaseModel):
|
||||
"""
|
||||
### [Response] API End-Point
|
||||
|
||||
**정상처리**\n
|
||||
- result: true\n
|
||||
- error: null\n
|
||||
|
||||
**오류발생**\n
|
||||
- result: false\n
|
||||
- error: 오류내용\n
|
||||
"""
|
||||
result: bool = Field(True, description='처리상태(성공: true, 실패: false)', example=True)
|
||||
error: str = Field(None, description='오류내용(성공: null, 실패: 오류내용)', example='invalid data')
|
||||
|
||||
@staticmethod
|
||||
def set_error(error):
|
||||
ResponseBase.result = False
|
||||
ResponseBase.error = str(error)
|
||||
return ResponseBase
|
||||
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class TokenRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 토큰 정보
|
||||
"""
|
||||
Authorization: str = Field(None, description='인증키', example='Bearer [token]')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""
|
||||
### 유저 정보
|
||||
"""
|
||||
id: int = Field(None, description='Table Index', example='1')
|
||||
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
|
||||
status: UserStatusType = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
|
||||
login: UserLoginType = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
|
||||
account_type: AccountType = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
|
||||
account: str = Field(None, description='계정', example='user1@test.com')
|
||||
pw: str = Field(None, description='비밀번호', example='1234')
|
||||
email: str = Field(None, description='전자메일', example='user1@test.com')
|
||||
name: str = Field(None, description='이름', example='user1')
|
||||
sex: str = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
|
||||
rrn: str = Field(None, description='주민등록번호', example='123456-1234567')
|
||||
address: str = Field(None, description='주소', example='대구1')
|
||||
phone_number: str = Field(None, description='연락처', example='010-1234-1234')
|
||||
picture: str = Field(None, description='프로필사진', example='profile1.png')
|
||||
marketing_agree: bool = Field(False, description='마케팅동의 여부', example=False)
|
||||
# extra
|
||||
user_type: str = Field(None, description='유저종류', example=UserType.patient)
|
||||
major: str = Field(None, description='진료과목', example='정형외과')
|
||||
character: str = Field(None, description='아바타 정보', example='Male_CH01')
|
||||
room_id: int = Field(None, description='입장한 방번호', example='1')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserInfoRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 유저 정보
|
||||
"""
|
||||
data: UserInfo = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserLoginReq(BaseModel):
|
||||
"""
|
||||
### 유저 로그인 정보
|
||||
"""
|
||||
account: str = Field(None, description='계정', example='user1@test.com')
|
||||
pw: str = Field(None, description='비밀번호', example='1234')
|
||||
|
||||
|
||||
class UserRegisterReq(BaseModel):
|
||||
"""
|
||||
### [Request] 유저 등록
|
||||
"""
|
||||
# pip install 'pydantic'
|
||||
# users
|
||||
status: Optional[str] = Field(UserStatusType.blocked, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
|
||||
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
|
||||
account: str = Field(description='계정', example='test@test.com')
|
||||
pw: str = Field(None, description='비밀번호', example='1234')
|
||||
email: Optional[str] = Field(None, description='전자메일', example='test@test.com')
|
||||
name: str = Field(description='이름', example='test')
|
||||
sex: str = Field(SexType.male, description='성별' + SexType.get_elements_str(), example=SexType.male)
|
||||
rrn: Optional[str] = Field(None, description='주민등록번호', example='19910101-1234567')
|
||||
address: Optional[str] = Field(None, description='주소', example='대구광역시 동구 동촌로 351, 4층 (용계동, 에이스빌딩)')
|
||||
phone_number: Optional[str] = Field(None, description='휴대전화', example='010-1234-1234')
|
||||
picture: Optional[str] = Field(None, description='사진', example='profile1.png')
|
||||
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
|
||||
# extra
|
||||
user_type: str = Field(UserType.patient, description='유저종류', example=UserType.patient)
|
||||
major: Optional[str] = Field(None, description='진료과목', example='정형외과')
|
||||
character: Optional[str] = Field(default='Male_CH01', description='아바타 정보', example='Male_CH01')
|
||||
# room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
|
||||
|
||||
|
||||
class UserSearchReq(BaseModel):
|
||||
"""
|
||||
### [Request] 유저 검색
|
||||
"""
|
||||
id: Optional[int] = Field(None, description='등록번호', example='1')
|
||||
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
|
||||
status: Optional[str] = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
|
||||
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
|
||||
account_type: Optional[str] = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
|
||||
account: Optional[str] = Field(None, description='계정', example='user1@test.com')
|
||||
account__like: Optional[str] = Field(None, description='계정 부분검색', example='%user%')
|
||||
email: Optional[str] = Field(None, description='전자메일', example='user1@test.com')
|
||||
email__like: Optional[str] = Field(None, description='전자메일 부분검색', example='%user%')
|
||||
name: Optional[str] = Field(None, description='이름', example='test')
|
||||
name__like: Optional[str] = Field(None, description='이름 부분검색', example='%user%')
|
||||
sex: Optional[str] = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
|
||||
rrn: Optional[str] = Field(None, description='주민등록번호', example='123456-1234567')
|
||||
address: Optional[str] = Field(None, description='주소', example='대구1')
|
||||
address__like: Optional[str] = Field(None, description='주소 부분검색', example='%대구%')
|
||||
phone_number: Optional[str] = Field(None, description='연락처', example='010-1234-1234')
|
||||
picture: Optional[str] = Field(None, description='프로필사진', example='profile1.png')
|
||||
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
|
||||
# extra
|
||||
user_type: Optional[str] = Field(None, description='유저종류' + UserType.get_elements_str(), example=UserType.patient)
|
||||
major: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
|
||||
character: Optional[str] = Field(None, description='아바타 정보', example='Male_CH01')
|
||||
room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserSearchRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 유저 검색
|
||||
"""
|
||||
data: List[UserInfo] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserUpdateReq(BaseModel):
|
||||
"""
|
||||
### 유저정보 변경
|
||||
"""
|
||||
status: Optional[str] = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
|
||||
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
|
||||
account_type: Optional[str] = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
|
||||
account: Optional[str] = Field(None, description='계정', example='user1@test.com')
|
||||
# pw: Optional[str] = Field(None, description='비밀번호', example='1234')
|
||||
name: Optional[str] = Field(None, description='이름', example='test')
|
||||
sex: Optional[str] = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
|
||||
rrn: Optional[str] = Field(None, description='주민등록번호', example='123456-1234567')
|
||||
address: Optional[str] = Field(None, description='주소', example='대구1')
|
||||
phone_number: Optional[str] = Field(None, description='연락처', example='010-1234-1234')
|
||||
picture: Optional[str] = Field(None, description='프로필사진', example='profile1.png')
|
||||
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
|
||||
# extra
|
||||
user_type: Optional[str] = Field(None, description='유저종류', example='patient')
|
||||
major: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
|
||||
character: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
|
||||
room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserUpdateMultiReq(BaseModel):
|
||||
"""
|
||||
### [Request] 유저 변경 (multi)
|
||||
"""
|
||||
search_info: UserSearchReq = None
|
||||
update_info: UserUpdateReq = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserUpdatePWReq(BaseModel):
|
||||
"""
|
||||
### [Request] 유저 비밀번호 변경
|
||||
"""
|
||||
account: str = Field(None, description='계정', example='user1@test.com')
|
||||
current_pw: str = Field(None, description='현재 비밀번호', example='1234')
|
||||
new_pw: str = Field(None, description='신규 비밀번호', example='5678')
|
||||
|
||||
|
||||
class AppointmentInfo(BaseModel):
|
||||
"""
|
||||
### 진료예약 정보
|
||||
"""
|
||||
id: int = Field(None, description='Table Index', example='1')
|
||||
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
|
||||
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
doctor_account: str = Field(None, description='의사 계정', example='doctor1@doctor.com')
|
||||
patient_account: str = Field(None, description='환자 계정', example='test@test.com')
|
||||
treatment_subject: str = Field(None, description='진료과목', example='정형외과')
|
||||
date: datetime = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
|
||||
status: str = Field(None, description='예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentInfoRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 진료예약 정보
|
||||
"""
|
||||
data: AppointmentInfo = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentRegisterReq(BaseModel):
|
||||
"""
|
||||
### [Request] 진료예약 등록
|
||||
"""
|
||||
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
doctor_account: str = Field(None, description='의사 계정', example='doctor1@doctor.com')
|
||||
patient_account: str = Field(None, description='환자 계정', example='test@test.com')
|
||||
treatment_subject: str = Field(None, description='진료과목', example='정형외과')
|
||||
date: datetime = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
|
||||
status: Optional[str] = Field(AppointmentStatusType.wait, description='예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentSearchReq(BaseModel):
|
||||
"""
|
||||
### [Request] 진료예약 검색
|
||||
"""
|
||||
id: Optional[int] = Field(None, description='등록번호', example='1')
|
||||
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
|
||||
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
doctor_account: Optional[str] = Field(None, description='의사 계정', example='doctor1@doctor.com')
|
||||
patient_account: Optional[str] = Field(None, description='환자 계정', example='test@test.com')
|
||||
treatment_subject: Optional[str] = Field(None, description='진료과목', example='정형외과')
|
||||
status: Optional[str] = Field(None, description='진료예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
|
||||
date: Optional[datetime] = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
|
||||
date__gt: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 이후)', example='2022-01-01T10:00:00')
|
||||
date__gte: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 포함 이후)', example='2022-01-01T10:00:00')
|
||||
date__lt: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 이전)', example='2022-01-01T10:00:00')
|
||||
date__lte: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 포함 이전)', example='2022-01-01T10:00:00')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentSearchRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 진료예약 검색
|
||||
"""
|
||||
data: List[AppointmentInfo] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentUpdateReq(BaseModel):
|
||||
"""
|
||||
### [Request] 진료예약 변경
|
||||
"""
|
||||
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
doctor_account: Optional[str] = Field(None, description='의사 계정', example='doctor1@doctor.com')
|
||||
patient_account: Optional[str] = Field(None, description='환자 계정', example='test@test.com')
|
||||
treatment_subject: Optional[str] = Field(None, description='진료과목', example='정형외과')
|
||||
date: Optional[datetime] = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
|
||||
status: Optional[str] = Field(None, description='진료예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class AppointmentUpdateMultiReq(BaseModel):
|
||||
"""
|
||||
### [Request] 라이선스 변경 (multi)
|
||||
"""
|
||||
search_info: AppointmentSearchReq = None
|
||||
update_info: AppointmentUpdateReq = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomInfo(BaseModel):
|
||||
"""
|
||||
### 방 정보
|
||||
"""
|
||||
id: int = Field(None, description='Table Index', example='1')
|
||||
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
|
||||
type: str = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
|
||||
auto_enter: bool = Field(None, description='방 생성후, 자동 입장', example='true')
|
||||
auto_delete: bool = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
|
||||
name: str = Field(None, description='방제목', example='홍삼원외과')
|
||||
pw: str = Field(None, description='비밀번호', example='1234')
|
||||
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
status: str = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
|
||||
cur_person: int = Field(None, description='현재 인원수', example='1')
|
||||
max_person: int = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
|
||||
desc: str = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomInfoRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 방 정보
|
||||
"""
|
||||
data: RoomInfo = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomRegisterReq(BaseModel):
|
||||
"""
|
||||
### [Request] 방 등록
|
||||
"""
|
||||
type: str = Field(RoomType.meeting, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
|
||||
auto_enter: Optional[bool] = Field(True, description='방 생성후, 자동 입장', example='true')
|
||||
auto_delete: Optional[bool] = Field(True, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
|
||||
name: str = Field(None, description='방제목', example='홍삼원외과')
|
||||
pw: str = Field(description='비밀번호', example='1234')
|
||||
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
status: Optional[str] = Field(RoomStatusType.open, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
|
||||
max_person: Optional[int] = Field(MEETING_ROOM_MAX_PERSON, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
|
||||
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomSearchReq(BaseModel):
|
||||
"""
|
||||
### [Request] 방 검색
|
||||
"""
|
||||
id: Optional[int] = Field(None, description='등록번호', example='1')
|
||||
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
|
||||
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
|
||||
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
|
||||
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
|
||||
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
|
||||
|
||||
type: Optional[str] = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
|
||||
auto_enter: Optional[bool] = Field(None, description='방 생성후, 자동 입장', example='true')
|
||||
auto_delete: Optional[bool] = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
|
||||
name: Optional[str] = Field(None, description='방제목', example='홍삼원외과')
|
||||
name__like: Optional[str] = Field(None, description='방제목 부분 검색(sql문법)', example='%외과%')
|
||||
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
status: Optional[str] = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
|
||||
cur_person: Optional[int] = Field(None, description='현재 인원수', example='1')
|
||||
max_person: Optional[int] = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
|
||||
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
|
||||
desc__like: Optional[str] = Field(None, description='부가정보 부분 검색(sql문법)', example='%영업%')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomSearchRes(ResponseBase):
|
||||
"""
|
||||
### [Response] 방 검색
|
||||
"""
|
||||
data: List[RoomInfo] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomUpdateReq(BaseModel):
|
||||
"""
|
||||
### [Request] 방 수정
|
||||
"""
|
||||
type: Optional[str] = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
|
||||
auto_enter: Optional[bool] = Field(None, description='방 생성후, 자동 입장', example='true')
|
||||
auto_delete: Optional[bool] = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
|
||||
name: Optional[str] = Field(None, description='방제목', example='홍삼원외과')
|
||||
pw: Optional[str] = Field(description='비밀번호', example='1234')
|
||||
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
|
||||
status: Optional[str] = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
|
||||
cur_person: Optional[int] = Field(None, description='현재 인원수', example='1')
|
||||
max_person: Optional[int] = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
|
||||
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RoomUpdateMultiReq(BaseModel):
|
||||
"""
|
||||
### [Request] 방 변경 (multi)
|
||||
"""
|
||||
search_info: RoomSearchReq = None
|
||||
update_info: RoomUpdateReq = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
195
fast_api/app/routes/auth.py
Executable file
195
fast_api/app/routes/auth.py
Executable file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: auth.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: authentication api
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
import bcrypt
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.common import consts
|
||||
from app import models as M
|
||||
from app.database.conn import db
|
||||
from app.database.schema import Users, Room
|
||||
|
||||
|
||||
router = APIRouter(prefix='/auth')
|
||||
|
||||
|
||||
@router.get('/find-account/{account}', response_model=M.ResponseBase, summary='계정유무 검사')
|
||||
async def find_account(account: str):
|
||||
"""
|
||||
## 계정유무 검사
|
||||
|
||||
주어진 계정이 존재하면 true, 없으면 false 처리
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
try:
|
||||
search_info = Users.get(account=account)
|
||||
if not search_info:
|
||||
raise Exception(f'not found data: {account}')
|
||||
return M.ResponseBase()
|
||||
except Exception as e:
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/register/{account_type}', status_code=201, response_model=M.TokenRes, summary='회원가입')
|
||||
async def register(account_type: M.AccountType, request_body_info: M.UserRegisterReq, session: Session = Depends(db.session)):
|
||||
"""
|
||||
## 회원가입 (신규등록)
|
||||
|
||||
**결과**
|
||||
- TokenRes
|
||||
"""
|
||||
new_user = None
|
||||
|
||||
try:
|
||||
# request
|
||||
if not request_body_info.account or not request_body_info.pw:
|
||||
raise Exception('Account and PW must be provided')
|
||||
|
||||
register_user = Users.get(account=request_body_info.account)
|
||||
if register_user:
|
||||
raise Exception(f'exist email: {request_body_info.account}')
|
||||
|
||||
if account_type == M.AccountType.email:
|
||||
hash_pw = None
|
||||
if request_body_info.pw:
|
||||
hash_pw = bcrypt.hashpw(request_body_info.pw.encode('utf-8'), bcrypt.gensalt())
|
||||
|
||||
# create users
|
||||
new_user = Users.create(session, auto_commit=True,
|
||||
status=request_body_info.status,
|
||||
account_type=account_type,
|
||||
account=request_body_info.account,
|
||||
pw=hash_pw,
|
||||
email=request_body_info.email,
|
||||
name=request_body_info.name,
|
||||
sex=request_body_info.sex,
|
||||
rrn=request_body_info.rrn,
|
||||
address=request_body_info.address,
|
||||
phone_number=request_body_info.phone_number,
|
||||
picture=request_body_info.picture,
|
||||
# extra
|
||||
user_type=request_body_info.user_type,
|
||||
major=request_body_info.major,
|
||||
character=request_body_info.character
|
||||
)
|
||||
|
||||
token = dict(Authorization=f'Bearer {create_access_token(data=M.UserToken.from_orm(new_user).dict(exclude={"pw", "marketing_agree"}),)}')
|
||||
return token
|
||||
|
||||
raise Exception('not supported')
|
||||
except Exception as e:
|
||||
if new_user:
|
||||
new_user.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/login/{account_type}', status_code=200, response_model=M.TokenRes, summary='사용자 접속')
|
||||
async def login(account_type: M.AccountType, request_body_info: M.UserLoginReq):
|
||||
"""
|
||||
## 사용자 접속
|
||||
|
||||
**결과**
|
||||
- TokenRes
|
||||
"""
|
||||
login_user = None
|
||||
update_user = None
|
||||
|
||||
try:
|
||||
# request
|
||||
if not request_body_info.account:
|
||||
raise Exception('invalid request: account')
|
||||
|
||||
if account_type == M.AccountType.email:
|
||||
# basic auth
|
||||
login_user = Users.get(account=request_body_info.account)
|
||||
if not login_user:
|
||||
raise Exception('not found user')
|
||||
|
||||
# check pw
|
||||
if not request_body_info.pw:
|
||||
raise Exception('invalid request: pw')
|
||||
is_verified = bcrypt.checkpw(request_body_info.pw.encode('utf-8'), login_user.pw.encode('utf-8'))
|
||||
if not is_verified:
|
||||
raise Exception('invalid password')
|
||||
|
||||
# TODO(hsj100): LOGIN_STATUS
|
||||
update_user = Users.filter(account=request_body_info.account)
|
||||
update_user.update(auto_commit=True, login='login')
|
||||
|
||||
token = dict(Authorization=f'Bearer {create_access_token(data=M.UserToken.from_orm(login_user).dict(exclude={"pw", "marketing_agree"}), )}')
|
||||
|
||||
return token
|
||||
|
||||
return M.ResponseBase.set_error('not supported')
|
||||
except Exception as e:
|
||||
if update_user:
|
||||
update_user.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/login_out/{user_email}', status_code=200, response_model=M.TokenRes, summary='사용자 접속종료')
|
||||
async def login_out(user_email: str):
|
||||
"""
|
||||
## 사용자 접속종료
|
||||
|
||||
** 방소유권을 가진 유저가 접속종료시에는 방삭제가 되며, 방에 속한 유저들의 방번호도 초기화 된다. **
|
||||
|
||||
현재 버전에서는 로그인/로그아웃의 상태를 유지하지 않고 상태값만을 서버에서 사용하기 때문에,\n
|
||||
***로그상태는 실제상황과 다를 수 있다.***
|
||||
|
||||
정상처리시 Authorization(null) 반환
|
||||
|
||||
**결과**
|
||||
- TokenRes
|
||||
"""
|
||||
user_info = None
|
||||
|
||||
try:
|
||||
# TODO(hsj100): LOGIN_STATUS
|
||||
user_info = Users.get(email=user_email)
|
||||
if not user_info:
|
||||
raise Exception('not found user')
|
||||
|
||||
# check room (creator)
|
||||
room_info = Room.get(create_account=user_email)
|
||||
if room_info:
|
||||
# initialize the room index of users
|
||||
search_info = Users.filter(room_id=room_info.id)
|
||||
search_info.update(auto_commit=True, synchronize_session=False, room_id=None)
|
||||
# delete room
|
||||
# search
|
||||
Room.filter(id=room_info.id).delete(auto_commit=True, synchronize_session=False)
|
||||
|
||||
Users.filter(email=user_email).update(auto_commit=True, login='logout')
|
||||
return M.TokenRes()
|
||||
except Exception as e:
|
||||
if user_info:
|
||||
user_info.close()
|
||||
return M.ResponseBase.set_error(e)
|
||||
|
||||
|
||||
|
||||
async def is_account_exist(account: str):
|
||||
get_account = Users.get(account=account)
|
||||
return True if get_account else False
|
||||
|
||||
|
||||
def create_access_token(*, data: dict = None, expires_delta: int = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
to_encode.update({'exp': datetime.utcnow() + timedelta(hours=expires_delta)})
|
||||
encoded_jwt = jwt.encode(to_encode, consts.JWT_SECRET, algorithm=consts.JWT_ALGORITHM)
|
||||
return encoded_jwt
|
||||
235
fast_api/app/routes/dev.py
Normal file
235
fast_api/app/routes/dev.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: dev.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: Developments Test
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
import bcrypt
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.requests import Request
|
||||
from inspect import currentframe as frame
|
||||
import uuid
|
||||
|
||||
from app import models as M
|
||||
from app.database.conn import db, Base
|
||||
from app.database.schema import Users, Appointment, Room
|
||||
|
||||
|
||||
# mail test
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
|
||||
def send_mail():
|
||||
"""
|
||||
구글 계정사용시 : 보안 수준이 낮은 앱에서의 접근 활성화
|
||||
|
||||
:return:
|
||||
"""
|
||||
sender = 'jolimola@gmail.com'
|
||||
sender_pw = '!ghkdtmdwns1'
|
||||
# recipient = 'hsj100@a2tec.co.kr'
|
||||
recipient = 'jwkim@daooldns.co.kr'
|
||||
list_cc = ['cc1@gmail.com', 'cc2@naver.com']
|
||||
str_cc = ','.join(list_cc)
|
||||
|
||||
title = 'Test mail'
|
||||
contents = '''
|
||||
This is test mail
|
||||
using smtplib.
|
||||
'''
|
||||
|
||||
smtp_server = smtplib.SMTP( # 1
|
||||
host='smtp.gmail.com',
|
||||
port=587
|
||||
)
|
||||
|
||||
smtp_server.ehlo() # 2
|
||||
smtp_server.starttls() # 2
|
||||
smtp_server.ehlo() # 2
|
||||
smtp_server.login(sender, sender_pw) # 3
|
||||
|
||||
msg = MIMEMultipart() # 4
|
||||
msg['From'] = sender # 5
|
||||
msg['To'] = recipient # 5
|
||||
# msg['Cc'] = str_cc # 5
|
||||
msg['Subject'] = contents # 5
|
||||
msg.attach(MIMEText(contents, 'plain')) # 6
|
||||
|
||||
smtp_server.send_message(msg) # 7
|
||||
smtp_server.quit() # 8
|
||||
|
||||
|
||||
router = APIRouter(prefix='/dev')
|
||||
|
||||
|
||||
@router.get('/test', summary='테스트', response_model=M.SWInfo)
|
||||
async def test(request: Request):
|
||||
"""
|
||||
## ELB 상태 체크용 API
|
||||
|
||||
**결과**
|
||||
- SWInfo
|
||||
"""
|
||||
# send_mail()
|
||||
# print('state.user', request.state.user)
|
||||
# try:
|
||||
# a = 1/0
|
||||
# except Exception as e:
|
||||
# request.state.inspect = frame()
|
||||
# raise e
|
||||
result_info = dict(uuid=uuid.uuid1())
|
||||
t = len(str(result_info['uuid']))
|
||||
return result_info
|
||||
|
||||
return M.SWInfo()
|
||||
|
||||
|
||||
@router.post('/db/add_test_data', summary='[DB] 테스트 데이터 생성', response_model=M.ResponseBase)
|
||||
async def db_add_test_data(session: Session = Depends(db.session)):
|
||||
"""
|
||||
## 테스트 데이터 생성
|
||||
|
||||
프로젝트(DATABASE)에 테스트 데이터를 생성한다.
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
# user
|
||||
hash_pw = bcrypt.hashpw(str('1234').encode('utf-8'), bcrypt.gensalt())
|
||||
user_info = Users.create(session=session, auto_commit=True
|
||||
, status=M.UserStatusType.active
|
||||
, account='user1@test.com', pw=hash_pw
|
||||
, email='user1@test.com'
|
||||
, name='user1', sex=M.SexType.male
|
||||
, rrn='111111-1000001', address='대구광역시 동구 1'
|
||||
, phone_number='010-1111-1111', picture='Boy_1.png.png'
|
||||
,user_type = 'patient', major = None, charactor = 'Male_ch01')
|
||||
|
||||
user_info = Users.create(session=session, auto_commit=True
|
||||
, status=M.UserStatusType.active
|
||||
, account='user2@test.com', pw=hash_pw
|
||||
, email='user2@test.com'
|
||||
, name='user2', sex=M.SexType.female
|
||||
, rrn='111111-1000002', address='대구광역시 북구 2'
|
||||
, phone_number='010-1111-2222', picture='Girl_1.png'
|
||||
, user_type='patient', major=None, charactor='Female_ch01')
|
||||
|
||||
user_info = Users.create(session=session, auto_commit=True
|
||||
, status=M.UserStatusType.active
|
||||
, account='doctor1@doctor.com', pw=hash_pw
|
||||
, email='doctor1@doctor.com'
|
||||
, name='doctor1', sex=M.SexType.male
|
||||
, rrn='111111-2000002', address='대구광역시 서구 3'
|
||||
, phone_number='010-1111-3333', picture='Boy_2.png'
|
||||
, user_type='doctor', major='정형외과', charactor='Male_ch02')
|
||||
|
||||
user_info = Users.create(session=session, auto_commit=True
|
||||
, status=M.UserStatusType.active
|
||||
, account='doctor2@doctor.com', pw=hash_pw
|
||||
, email='doctor2@doctor.com'
|
||||
, name='doctor2', sex=M.SexType.female
|
||||
, rrn='111111-2000004', address='대구광역시 수성구 4'
|
||||
, phone_number='010-1111-4444', picture='Girl_2.png'
|
||||
, user_type='doctor', major='산부인과', charactor='Female_ch02')
|
||||
|
||||
Appointment.create(session=session, auto_commit=True,
|
||||
create_account='doctor1@doctor.com',
|
||||
doctor_account='doctor1@doctor.com',
|
||||
patient_account='user1@test.com',
|
||||
treatment_subject='정형외과',
|
||||
date='2022-01-30 10:00.00')
|
||||
Appointment.create(session=session, auto_commit=True,
|
||||
create_account='doctor1@doctor.com',
|
||||
doctor_account='doctor1@doctor.com',
|
||||
patient_account='user2@test.com',
|
||||
treatment_subject='정형외과',
|
||||
date='2022-02-10 16:30.00')
|
||||
Appointment.create(session=session, auto_commit=True,
|
||||
create_account='doctor2@doctor.com',
|
||||
doctor_account='doctor2@doctor.com',
|
||||
patient_account='user1@test.com',
|
||||
treatment_subject='산부인과',
|
||||
date='2022-03-30 11:00.00')
|
||||
Appointment.create(session=session, auto_commit=True,
|
||||
create_account='doctor2@doctor.com',
|
||||
doctor_account='doctor2@doctor.com',
|
||||
patient_account='user2@test.com',
|
||||
treatment_subject='산부인과',
|
||||
date='2022-04-10 13:30.00')
|
||||
|
||||
return M.ResponseBase()
|
||||
|
||||
|
||||
@router.post('/db/delete_all_data', summary='[DB] 데이터 삭제[전체]', response_model=M.ResponseBase)
|
||||
async def db_delete_all_data():
|
||||
"""
|
||||
## DB 데이터 삭제
|
||||
|
||||
프로젝트(DATABASE)의 모든 테이블의 내용을 삭제한다. (테이블은 유지)
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
engine = db.engine
|
||||
metadata = Base.metadata
|
||||
|
||||
foreign_key_turn_off = {
|
||||
'mysql': 'SET FOREIGN_KEY_CHECKS=0;',
|
||||
'postgresql': 'SET CONSTRAINTS ALL DEFERRED;',
|
||||
'sqlite': 'PRAGMA foreign_keys = OFF;',
|
||||
}
|
||||
foreign_key_turn_on = {
|
||||
'mysql': 'SET FOREIGN_KEY_CHECKS=1;',
|
||||
'postgresql': 'SET CONSTRAINTS ALL IMMEDIATE;',
|
||||
'sqlite': 'PRAGMA foreign_keys = ON;',
|
||||
}
|
||||
truncate_query = {
|
||||
'mysql': 'TRUNCATE TABLE {};',
|
||||
'postgresql': 'TRUNCATE TABLE {} RESTART IDENTITY CASCADE;',
|
||||
'sqlite': 'DELETE FROM {};',
|
||||
}
|
||||
|
||||
with engine.begin() as conn:
|
||||
conn.execute(foreign_key_turn_off[engine.name])
|
||||
|
||||
for table in reversed(metadata.sorted_tables):
|
||||
conn.execute(truncate_query[engine.name].format(table.name))
|
||||
|
||||
conn.execute(foreign_key_turn_on[engine.name])
|
||||
|
||||
return M.ResponseBase()
|
||||
|
||||
|
||||
@router.post('/db/delete_all_tables', summary='[DB] 테이블 삭제[전체]', response_model=M.ResponseBase)
|
||||
async def db_delete_all_tables():
|
||||
"""
|
||||
## DB 데이터 삭제
|
||||
|
||||
프로젝트(DATABASE)의 모든 테이블을 삭제한다.
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
engine = db.engine
|
||||
metadata = Base.metadata
|
||||
|
||||
truncate_query = {
|
||||
'mysql': 'DROP TABLE IF EXISTS {};',
|
||||
'postgresql': 'DROP TABLE IF EXISTS {};',
|
||||
'sqlite': 'DROP TABLE IF EXISTS {};',
|
||||
}
|
||||
|
||||
with engine.begin() as conn:
|
||||
for table in reversed(metadata.sorted_tables):
|
||||
conn.execute(truncate_query[engine.name].format(table.name))
|
||||
|
||||
return M.ResponseBase()
|
||||
32
fast_api/app/routes/index.py
Executable file
32
fast_api/app/routes/index.py
Executable file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: index.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: basic & test api
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.utils.date_utils import D
|
||||
from app.models import SWInfo
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/', summary='서비스 정보', response_model=SWInfo)
|
||||
async def index():
|
||||
"""
|
||||
## 서비스 정보
|
||||
소프트웨어 이름, 버전정보, 현재시간
|
||||
|
||||
**결과**
|
||||
- SWInfo
|
||||
"""
|
||||
sw_info = SWInfo()
|
||||
sw_info.date = D.date_str()
|
||||
return sw_info
|
||||
384
fast_api/app/routes/services.py
Executable file
384
fast_api/app/routes/services.py
Executable file
@@ -0,0 +1,384 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: services.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: services api
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.requests import Request
|
||||
|
||||
from app import models as M
|
||||
from app.database.conn import db
|
||||
from app.common.config import conf
|
||||
from app.database.schema import Users, Appointment, Room
|
||||
from app.database.crud import table_read, table_update, table_delete
|
||||
|
||||
|
||||
router = APIRouter(prefix='/services')
|
||||
|
||||
|
||||
@router.post('/appointment/search', response_model=M.AppointmentSearchRes, summary='진료예약 검색')
|
||||
async def search_appointment(request: Request, request_body_info: M.AppointmentSearchReq):
|
||||
"""
|
||||
## 진료예약 검색
|
||||
|
||||
검색에 필요한 항목들을 Request body 에 사용한다.\n
|
||||
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
|
||||
|
||||
**전체검색**\n
|
||||
empty object 사용 ( {} )\n
|
||||
예) Request body: {}\n
|
||||
|
||||
**검색항목**\n
|
||||
- 부분검색 항목\n
|
||||
SQL 문법( %, _ )을 사용한다.\n
|
||||
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
|
||||
|
||||
- 구간검색 항목
|
||||
* __lt: 주어진 값보다 작은값
|
||||
* __lte: 주어진 값보다 같거나 작은 값
|
||||
* __gt: 주어진 값보다 큰값
|
||||
* __gte: 주어진 값보다 같거나 큰값
|
||||
|
||||
**결과**
|
||||
- AppointmentSearchRes
|
||||
"""
|
||||
# result
|
||||
return await table_read(request.state.user, Appointment, request_body_info, M.AppointmentSearchRes, M.AppointmentInfo)
|
||||
|
||||
|
||||
@router.post('/appointment/add', response_model=M.AppointmentInfoRes, summary='진료예약 등록')
|
||||
async def add_appointment(request: Request, request_body_info: M.AppointmentRegisterReq, session: Session = Depends(db.session)):
|
||||
"""
|
||||
## 진료예약 등록
|
||||
|
||||
현재 접속 계정으로 진료예약정보를 등록한다.\n
|
||||
|
||||
**진료 상태(status) 흐름**\n
|
||||
- wait: 최초 진료예약 신청 상태
|
||||
- reserve: 해당 병원 조직원(doctor)에 의해서 예약 상태로 변경
|
||||
- cancel: 해당 병원 조직원(doctor)에 의해서 취소 상태로 변경
|
||||
- treatment: 해당 병원 조직원(doctor)에 의해서 진료완료 상태로 변경
|
||||
|
||||
**결과**
|
||||
- AppointmentInfoRes
|
||||
"""
|
||||
result_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
accessor_info = request.state.user
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
request_info = dict()
|
||||
for key, val in request_body_info.dict().items():
|
||||
if val:
|
||||
request_info[key] = val
|
||||
if not request_info:
|
||||
raise Exception('invalid request_body')
|
||||
|
||||
# check create_account
|
||||
user_info = Users.get(account=request_body_info.create_account)
|
||||
if not user_info:
|
||||
raise Exception(f'not found user: {request_body_info.create_account}')
|
||||
|
||||
# check doctor_account
|
||||
user_info = Users.get(account=request_body_info.doctor_account)
|
||||
if not user_info:
|
||||
raise Exception(f'not found user: {request_body_info.doctor_account}')
|
||||
|
||||
# check patient_account
|
||||
user_info = Users.get(account=request_body_info.patient_account)
|
||||
if not user_info:
|
||||
raise Exception(f'not found user: {request_body_info.patient_account}')
|
||||
|
||||
# create
|
||||
result_info = Appointment.create(session=session, auto_commit=True,
|
||||
create_account=request_body_info.create_account,
|
||||
doctor_account=request_body_info.doctor_account,
|
||||
patient_account=request_body_info.patient_account,
|
||||
treatment_subject=request_body_info.treatment_subject,
|
||||
date=request_body_info.date,
|
||||
status=request_body_info.status)
|
||||
|
||||
# result
|
||||
return M.AppointmentInfoRes(data=result_info)
|
||||
except Exception as e:
|
||||
if result_info:
|
||||
result_info.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
|
||||
@router.put('/appointment/update', response_model=M.ResponseBase, summary='진료예약 변경')
|
||||
async def update_appointment(request: Request, request_body_info: M.AppointmentUpdateMultiReq):
|
||||
"""
|
||||
## 진료예약 변경
|
||||
|
||||
**search_info**: 변경대상\n
|
||||
|
||||
**update_info**: 변경내용\n
|
||||
- **비밀번호** 제외
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
# result
|
||||
return await table_update(request.state.user, Appointment, request_body_info, M.ResponseBase)
|
||||
|
||||
|
||||
@router.delete('/appointment/delete', response_model=M.ResponseBase, summary='진료예약 삭제')
|
||||
async def delete_appointment_info(request: Request, request_body_info: M.AppointmentSearchReq):
|
||||
"""
|
||||
## 진료예약 삭제
|
||||
|
||||
조건에 해당하는 유저정보를 모두 삭제한다.\n
|
||||
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
|
||||
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
|
||||
|
||||
`유저삭제시 관계 테이블의 정보도 같이 삭제된다.`
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
# result
|
||||
return await table_delete(request.state.user, Appointment, request_body_info, M.ResponseBase)
|
||||
|
||||
|
||||
@router.post('/room/search', response_model=M.RoomSearchRes, summary='방 검색')
|
||||
async def get_room(request: Request, request_body_info: M.RoomSearchReq):
|
||||
"""
|
||||
## 방 검색
|
||||
|
||||
검색에 필요한 항목들을 Request body 에 사용한다.\n
|
||||
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
|
||||
|
||||
**전체검색**\n
|
||||
empty object 사용 ( {} )\n
|
||||
예) Request body: {}\n
|
||||
|
||||
**검색항목**\n
|
||||
- 부분검색 항목\n
|
||||
SQL 문법( %, _ )을 사용한다.\n
|
||||
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
|
||||
|
||||
- 구간검색 항목
|
||||
* __lt: 주어진 값보다 작은값
|
||||
* __lte: 주어진 값보다 같거나 작은 값
|
||||
* __gt: 주어진 값보다 큰값
|
||||
* __gte: 주어진 값보다 같거나 큰값
|
||||
|
||||
**결과**
|
||||
- RoomSearchRes
|
||||
"""
|
||||
# result
|
||||
return await table_read(request.state.user, Room, request_body_info, M.RoomSearchRes, M.RoomInfo)
|
||||
|
||||
|
||||
@router.post('/room/add', response_model=M.RoomInfoRes, summary='방 생성')
|
||||
async def add_room(request: Request, request_body_info: M.RoomRegisterReq, session: Session = Depends(db.session)):
|
||||
"""
|
||||
## 방 등록
|
||||
|
||||
현재 접속 계정으로 방을 생성한다.\n
|
||||
|
||||
**방 상태(status) 흐름**\n
|
||||
- open: 입장가능
|
||||
- close: 입장불가
|
||||
- tclose: 입장불가(일시적으로 입장불가 상태로 할 경우)
|
||||
- full: 입장불가(만원)
|
||||
|
||||
**결과**
|
||||
- RoomInfoRes
|
||||
"""
|
||||
result_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
accessor_info = request.state.user
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
request_info = dict()
|
||||
for key, val in request_body_info.dict().items():
|
||||
if val:
|
||||
request_info[key] = val
|
||||
if not request_info:
|
||||
raise Exception('invalid request_body')
|
||||
|
||||
# check creator
|
||||
user_info = Users.get(account=request_body_info.create_account)
|
||||
if not user_info:
|
||||
raise Exception(f'not found user: {request_body_info.create_account}')
|
||||
|
||||
# check conditions
|
||||
search_info = Room.filter(create_account=request_body_info.create_account).all()
|
||||
if len(search_info) > 0:
|
||||
raise Exception(f'already exists the rooms in user: {len(search_info)}')
|
||||
|
||||
# create
|
||||
result_info = Room.create(session=session, auto_commit=True,
|
||||
type=request_body_info.type,
|
||||
name=request_body_info.name,
|
||||
pw=request_body_info.pw,
|
||||
create_account=request_body_info.create_account,
|
||||
status=request_body_info.status,
|
||||
cur_person=1 if request_body_info.auto_enter else 0,
|
||||
max_person=request_body_info.max_person,
|
||||
desc=request_body_info.desc)
|
||||
|
||||
# enter
|
||||
if request_body_info.auto_enter:
|
||||
Users.filter(account=request_body_info.create_account).update(auto_commit=True, synchronize_session=False, room_id=result_info.id)
|
||||
|
||||
# result
|
||||
return M.RoomInfoRes(data=result_info)
|
||||
except Exception as e:
|
||||
if result_info:
|
||||
result_info.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.put('/room/update', response_model=M.ResponseBase, summary='방 변경')
|
||||
async def update_room(request: Request, request_body_info: M.RoomUpdateMultiReq):
|
||||
"""
|
||||
## 방 변경
|
||||
|
||||
**search_info**: 변경대상\n
|
||||
|
||||
**update_info**: 변경내용\n
|
||||
- **비밀번호** 제외
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
return await table_update(request.state.user, Room, request_body_info, M.ResponseBase)
|
||||
|
||||
|
||||
@router.delete('/room/delete', response_model=M.ResponseBase, summary='방 삭제')
|
||||
async def delete_room(request: Request, request_body_info: M.RoomSearchReq):
|
||||
"""
|
||||
## 방 삭제
|
||||
조건에 해당하는 정보를 모두 삭제한다.\n
|
||||
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
|
||||
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
|
||||
|
||||
`삭제시, 이미 입장한 인원의 방번호는 전부 초기화 된다.`
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
search_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
accessor_info = request.state.user
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
|
||||
# initialize the room index of users
|
||||
search_info = Users.filter(room_id=request_body_info.id)
|
||||
search_info.update(auto_commit=True, synchronize_session=False, room_id=None)
|
||||
|
||||
# result
|
||||
return await table_delete(request.state.user, Room, request_body_info, M.ResponseBase)
|
||||
except Exception as e:
|
||||
if search_info:
|
||||
search_info.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/room/enter', response_model=M.RoomInfoRes, summary='방 입장')
|
||||
async def enter_room(request: Request, room_id: int = Query(None, description='입장할 방 번호'), room_pw: str = Query(None, description='비밀번호')):
|
||||
"""
|
||||
## 방 입장
|
||||
|
||||
**결과**
|
||||
- RoomInfoRes
|
||||
"""
|
||||
try:
|
||||
# check - user
|
||||
connect_user = request.state.user
|
||||
user_info = Users.get(account=connect_user.account)
|
||||
if not user_info:
|
||||
raise Exception('invalid user(current)')
|
||||
|
||||
# check - room
|
||||
room_info = Room.get(id=room_id)
|
||||
if not room_info:
|
||||
raise Exception('not found room')
|
||||
if room_pw != room_info.pw:
|
||||
raise Exception('invalid room_pw')
|
||||
|
||||
# check - number of person
|
||||
cur_person = room_info.cur_person
|
||||
if room_info.max_person <= room_info.cur_person:
|
||||
raise Exception(f'full! (cur_person: {room_info.cur_person})')
|
||||
|
||||
# check - creator
|
||||
is_creator = False
|
||||
if room_info.create_account == user_info.account:
|
||||
is_creator = True
|
||||
|
||||
if room_info.status != 'open' and not is_creator:
|
||||
raise Exception('abort entrance')
|
||||
|
||||
# update - user
|
||||
Users.filter(id=user_info.id).update(auto_commit=True, room_id=room_info.id)
|
||||
|
||||
# update - room
|
||||
user_num = Users.filter(room_id=room_info.id).count()
|
||||
result_room_info = Room.filter(id=room_info.id).update(auto_commit=True, cur_person=user_num)
|
||||
|
||||
# result
|
||||
return M.RoomInfoRes(data=result_room_info)
|
||||
except Exception as e:
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/room/leave', response_model=M.RoomInfoRes, summary='방 퇴장')
|
||||
async def leave_room(request: Request, room_id: int = Query(None, description='퇴장할 방 번호')):
|
||||
"""
|
||||
## 방 퇴장
|
||||
|
||||
**결과**
|
||||
- RoomInfoRes
|
||||
- 자동 방 삭제(**auto_delete**)가 될 경우에는 **data** 항목은 null로 반환
|
||||
"""
|
||||
try:
|
||||
# check - user
|
||||
connect_user = request.state.user
|
||||
user_info = Users.get(account=connect_user.account)
|
||||
if not user_info:
|
||||
raise Exception(f'not found user: {connect_user.account}')
|
||||
if user_info.room_id != room_id:
|
||||
raise Exception(f'invalid room: {user_info.room_id}')
|
||||
|
||||
# check - room
|
||||
room_info = Room.get(id=room_id)
|
||||
if not room_info:
|
||||
raise Exception('not found room')
|
||||
|
||||
# update - user
|
||||
Users.filter(id=user_info.id).update(auto_commit=True, room_id=None)
|
||||
|
||||
# update - room
|
||||
user_num = Users.filter(room_id=room_info.id).count()
|
||||
result_room_info = Room.filter(id=room_info.id).update(auto_commit=True, cur_person=user_num)
|
||||
|
||||
# delete - room
|
||||
if result_room_info.cur_person < 1:
|
||||
if room_info.auto_delete:
|
||||
Room.filter(id=room_info.id).delete(auto_commit=True)
|
||||
# result
|
||||
return M.ResponseBase()
|
||||
|
||||
# result
|
||||
return M.RoomInfoRes(data=result_room_info)
|
||||
except Exception as e:
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
275
fast_api/app/routes/users.py
Executable file
275
fast_api/app/routes/users.py
Executable file
@@ -0,0 +1,275 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: users.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: users api
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from starlette.requests import Request
|
||||
import bcrypt
|
||||
|
||||
from app import models as M
|
||||
from app.common.config import conf
|
||||
from app.database.schema import Users
|
||||
from app.database.crud import table_read, table_update, table_delete
|
||||
|
||||
|
||||
router = APIRouter(prefix='/user')
|
||||
|
||||
|
||||
@router.get('/me', response_model=M.UserSearchRes, summary='접속자 정보')
|
||||
async def get_me(request: Request):
|
||||
"""
|
||||
## 현재 접속된 자신정보 확인
|
||||
|
||||
**결과**
|
||||
- UserSearchRes
|
||||
"""
|
||||
target_table = Users
|
||||
search_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
if conf().GLOBAL_TOKEN:
|
||||
raise Exception('not supported: use search api!')
|
||||
|
||||
accessor_info = request.state.user
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
|
||||
# search
|
||||
search_info = target_table.get(account=accessor_info.account)
|
||||
if not search_info:
|
||||
raise Exception('not found data')
|
||||
|
||||
# result
|
||||
result_info = list()
|
||||
result_info.append(M.UserInfo.from_orm(search_info))
|
||||
return M.UserSearchRes(data=result_info)
|
||||
except Exception as e:
|
||||
if search_info:
|
||||
search_info.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.post('/search', response_model=M.UserSearchRes, summary='유저정보 검색')
|
||||
async def search_user(request: Request, request_body_info: M.UserSearchReq):
|
||||
"""
|
||||
## 유저정보 검색
|
||||
|
||||
검색에 필요한 항목들을 Request body 에 사용한다.\n
|
||||
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
|
||||
|
||||
**전체검색**\n
|
||||
empty object 사용 ( {} )\n
|
||||
예) Request body: {}\n
|
||||
|
||||
**검색항목**\n
|
||||
- 부분검색 항목\n
|
||||
SQL 문법( %, _ )을 사용한다.\n
|
||||
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
|
||||
|
||||
- 구간검색 항목
|
||||
* __lt: 주어진 값보다 작은값
|
||||
* __lte: 주어진 값보다 같거나 작은 값
|
||||
* __gt: 주어진 값보다 큰값
|
||||
* __gte: 주어진 값보다 같거나 큰값
|
||||
|
||||
**결과**
|
||||
- UserSearchRes
|
||||
"""
|
||||
return await table_read(request.state.user, Users, request_body_info, M.UserSearchRes, M.UserInfo)
|
||||
|
||||
|
||||
@router.put('/update', response_model=M.ResponseBase, summary='유저정보 변경')
|
||||
async def update_user(request: Request, request_body_info: M.UserUpdateMultiReq):
|
||||
"""
|
||||
## 유저정보 변경
|
||||
|
||||
**search_info**: 변경대상\n
|
||||
|
||||
**update_info**: 변경내용\n
|
||||
- **비밀번호** 제외
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
return await table_update(request.state.user, Users, request_body_info, M.ResponseBase)
|
||||
|
||||
|
||||
@router.put('/update_pw', response_model=M.ResponseBase, summary='유저 비밀번호 변경')
|
||||
async def update_user_pw(request: Request, request_body_info: M.UserUpdatePWReq):
|
||||
"""
|
||||
## 유저정보 비밀번호 변경
|
||||
|
||||
**account**의 **비밀번호**를 변경한다.
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
target_table = Users
|
||||
search_info = None
|
||||
|
||||
try:
|
||||
# request
|
||||
accessor_info = request.state.user
|
||||
if not accessor_info:
|
||||
raise Exception('invalid accessor')
|
||||
if not request_body_info.account:
|
||||
raise Exception('invalid account')
|
||||
|
||||
# search
|
||||
target_user = target_table.get(account=request_body_info.account)
|
||||
is_verified = bcrypt.checkpw(request_body_info.current_pw.encode('utf-8'), target_user.pw.encode('utf-8'))
|
||||
if not is_verified:
|
||||
raise Exception('invalid password')
|
||||
|
||||
search_info = target_table.filter(id=target_user.id)
|
||||
if not search_info.first():
|
||||
raise Exception('not found data')
|
||||
|
||||
# process
|
||||
hash_pw = bcrypt.hashpw(request_body_info.new_pw.encode('utf-8'), bcrypt.gensalt())
|
||||
result_info = search_info.update(auto_commit=True, pw=hash_pw)
|
||||
if not result_info or not result_info.id:
|
||||
raise Exception('failed update')
|
||||
|
||||
# result
|
||||
return M.ResponseBase()
|
||||
except Exception as e:
|
||||
if search_info:
|
||||
search_info.close()
|
||||
return M.ResponseBase.set_error(str(e))
|
||||
|
||||
|
||||
@router.delete('/delete', response_model=M.ResponseBase, summary='유저정보 삭제')
|
||||
async def delete_user(request: Request, request_body_info: M.UserSearchReq):
|
||||
"""
|
||||
## 유저정보 삭제
|
||||
조건에 해당하는 정보를 모두 삭제한다.\n
|
||||
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
|
||||
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
|
||||
|
||||
`유저삭제시 관계 테이블의 정보도 같이 삭제된다.`
|
||||
|
||||
**결과**
|
||||
- ResponseBase
|
||||
"""
|
||||
return await table_delete(request.state.user, Users, request_body_info, M.ResponseBase)
|
||||
|
||||
|
||||
# NOTE(hsj100): apikey
|
||||
"""
|
||||
"""
|
||||
# @router.get('/apikeys', response_model=List[M.GetApiKeyList])
|
||||
# async def get_api_keys(request: Request):
|
||||
# """
|
||||
# API KEY 조회
|
||||
# :param request:
|
||||
# :return:
|
||||
# """
|
||||
# user = request.state.user
|
||||
# api_keys = ApiKeys.filter(user_id=user.id).all()
|
||||
# return api_keys
|
||||
#
|
||||
#
|
||||
# @router.post('/apikeys', response_model=M.GetApiKeys)
|
||||
# async def create_api_keys(request: Request, key_info: M.AddApiKey, session: Session = Depends(db.session)):
|
||||
# """
|
||||
# API KEY 생성
|
||||
# :param request:
|
||||
# :param key_info:
|
||||
# :param session:
|
||||
# :return:
|
||||
# """
|
||||
# user = request.state.user
|
||||
#
|
||||
# api_keys = ApiKeys.filter(session, user_id=user.id, status='active').count()
|
||||
# if api_keys == MAX_API_KEY:
|
||||
# raise ex.MaxKeyCountEx()
|
||||
#
|
||||
# alphabet = string.ascii_letters + string.digits
|
||||
# s_key = ''.join(secrets.choice(alphabet) for _ in range(40))
|
||||
# uid = None
|
||||
# while not uid:
|
||||
# uid_candidate = f'{str(uuid4())[:-12]}{str(uuid4())}'
|
||||
# uid_check = ApiKeys.get(access_key=uid_candidate)
|
||||
# if not uid_check:
|
||||
# uid = uid_candidate
|
||||
#
|
||||
# key_info = key_info.dict()
|
||||
# new_key = ApiKeys.create(session, auto_commit=True, secret_key=s_key, user_id=user.id, access_key=uid, **key_info)
|
||||
# return new_key
|
||||
#
|
||||
#
|
||||
# @router.put('/apikeys/{key_id}', response_model=M.GetApiKeyList)
|
||||
# async def update_api_keys(request: Request, key_id: int, key_info: M.AddApiKey):
|
||||
# """
|
||||
# API KEY User Memo Update
|
||||
# :param request:
|
||||
# :param key_id:
|
||||
# :param key_info:
|
||||
# :return:
|
||||
# """
|
||||
# user = request.state.user
|
||||
# key_data = ApiKeys.filter(id=key_id)
|
||||
# if key_data and key_data.first().user_id == user.id:
|
||||
# return key_data.update(auto_commit=True, **key_info.dict())
|
||||
# raise ex.NoKeyMatchEx()
|
||||
#
|
||||
#
|
||||
# @router.delete('/apikeys/{key_id}')
|
||||
# async def delete_api_keys(request: Request, key_id: int, access_key: str):
|
||||
# user = request.state.user
|
||||
# await check_api_owner(user.id, key_id)
|
||||
# search_by_key = ApiKeys.filter(access_key=access_key)
|
||||
# if not search_by_key.first():
|
||||
# raise ex.NoKeyMatchEx()
|
||||
# search_by_key.delete(auto_commit=True)
|
||||
# return MessageOk()
|
||||
#
|
||||
#
|
||||
# @router.get('/apikeys/{key_id}/whitelists', response_model=List[M.GetAPIWhiteLists])
|
||||
# async def get_api_keys(request: Request, key_id: int):
|
||||
# user = request.state.user
|
||||
# await check_api_owner(user.id, key_id)
|
||||
# whitelists = ApiWhiteLists.filter(api_key_id=key_id).all()
|
||||
# return whitelists
|
||||
#
|
||||
#
|
||||
# @router.post('/apikeys/{key_id}/whitelists', response_model=M.GetAPIWhiteLists)
|
||||
# async def create_api_keys(request: Request, key_id: int, ip: M.CreateAPIWhiteLists, session: Session = Depends(db.session)):
|
||||
# user = request.state.user
|
||||
# await check_api_owner(user.id, key_id)
|
||||
# import ipaddress
|
||||
# try:
|
||||
# _ip = ipaddress.ip_address(ip.ip_addr)
|
||||
# except Exception as e:
|
||||
# raise ex.InvalidIpEx(ip.ip_addr, e)
|
||||
# if ApiWhiteLists.filter(api_key_id=key_id).count() == MAX_API_WHITELIST:
|
||||
# raise ex.MaxWLCountEx()
|
||||
# ip_dup = ApiWhiteLists.get(api_key_id=key_id, ip_addr=ip.ip_addr)
|
||||
# if ip_dup:
|
||||
# return ip_dup
|
||||
# ip_reg = ApiWhiteLists.create(session=session, auto_commit=True, api_key_id=key_id, ip_addr=ip.ip_addr)
|
||||
# return ip_reg
|
||||
#
|
||||
#
|
||||
# @router.delete('/apikeys/{key_id}/whitelists/{list_id}')
|
||||
# async def delete_api_keys(request: Request, key_id: int, list_id: int):
|
||||
# user = request.state.user
|
||||
# await check_api_owner(user.id, key_id)
|
||||
# ApiWhiteLists.filter(id=list_id, api_key_id=key_id).delete()
|
||||
#
|
||||
# return MessageOk()
|
||||
#
|
||||
#
|
||||
# async def check_api_owner(user_id, key_id):
|
||||
# api_keys = ApiKeys.get(id=key_id, user_id=user_id)
|
||||
# if not api_keys:
|
||||
# raise ex.NoKeyMatchEx()
|
||||
49
fast_api/app/utils/date_utils.py
Executable file
49
fast_api/app/utils/date_utils.py
Executable file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: date_utils.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: date-utility functions
|
||||
"""
|
||||
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
_TIMEDELTA = 9
|
||||
|
||||
class D:
|
||||
def __init__(self, *args):
|
||||
self.utc_now = datetime.utcnow()
|
||||
# NOTE(hsj100): utc->kst
|
||||
self.timedelta = _TIMEDELTA
|
||||
|
||||
@classmethod
|
||||
def datetime(cls, diff: int=_TIMEDELTA) -> datetime:
|
||||
return datetime.utcnow() + timedelta(hours=diff) if diff > 0 else datetime.now()
|
||||
|
||||
@classmethod
|
||||
def date(cls, diff: int=_TIMEDELTA) -> date:
|
||||
return cls.datetime(diff=diff).date()
|
||||
|
||||
@classmethod
|
||||
def date_num(cls, diff: int=_TIMEDELTA) -> int:
|
||||
return int(cls.date(diff=diff).strftime('%Y%m%d'))
|
||||
|
||||
@classmethod
|
||||
def validate(cls, date_text):
|
||||
try:
|
||||
datetime.strptime(date_text, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValueError('Incorrect data format, should be YYYY-MM-DD')
|
||||
|
||||
@classmethod
|
||||
def date_str(cls, diff: int = _TIMEDELTA) -> str:
|
||||
return cls.datetime(diff=diff).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
@classmethod
|
||||
def check_expire_date(cls, expire_date: datetime):
|
||||
td = expire_date - datetime.now()
|
||||
timestamp = td.total_seconds()
|
||||
return timestamp
|
||||
19
fast_api/app/utils/extra.py
Normal file
19
fast_api/app/utils/extra.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: extra.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: extra functions
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.common.consts import NUM_RETRY_UUID_GEN
|
||||
from app.database.schema import License
|
||||
from app.utils.date_utils import D
|
||||
from app import models as M
|
||||
|
||||
65
fast_api/app/utils/logger.py
Executable file
65
fast_api/app/utils/logger.py
Executable file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: logger.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: logger
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from time import time
|
||||
from fastapi.requests import Request
|
||||
from fastapi import Body
|
||||
from fastapi.logger import logger
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
async def api_logger(request: Request, response=None, error=None):
|
||||
time_format = '%Y/%m/%d %H:%M:%S'
|
||||
t = time() - request.state.start
|
||||
status_code = error.status_code if error else response.status_code
|
||||
error_log = None
|
||||
user = request.state.user
|
||||
if error:
|
||||
if request.state.inspect:
|
||||
frame = request.state.inspect
|
||||
error_file = frame.f_code.co_filename
|
||||
error_func = frame.f_code.co_name
|
||||
error_line = frame.f_lineno
|
||||
else:
|
||||
error_func = error_file = error_line = 'UNKNOWN'
|
||||
|
||||
error_log = dict(
|
||||
errorFunc=error_func,
|
||||
location='{} line in {}'.format(str(error_line), error_file),
|
||||
raised=str(error.__class__.__name__),
|
||||
msg=str(error.ex),
|
||||
)
|
||||
|
||||
account = user.account.split('@') if user and user.account else None
|
||||
user_log = dict(
|
||||
client=request.state.ip,
|
||||
user=user.id if user and user.id else None,
|
||||
account='**' + account[0][2:-1] + '*@' + account[1] if user and user.account else None,
|
||||
)
|
||||
|
||||
log_dict = dict(
|
||||
url=request.url.hostname + request.url.path,
|
||||
method=str(request.method),
|
||||
statusCode=status_code,
|
||||
errorDetail=error_log,
|
||||
client=user_log,
|
||||
processedTime=str(round(t * 1000, 5)) + 'ms',
|
||||
datetimeUTC=datetime.utcnow().strftime(time_format),
|
||||
datetimeKST=(datetime.utcnow() + timedelta(hours=9)).strftime(time_format),
|
||||
)
|
||||
if error and error.status_code >= 500:
|
||||
logger.error(json.dumps(log_dict))
|
||||
else:
|
||||
logger.info(json.dumps(log_dict))
|
||||
22
fast_api/app/utils/query_utils.py
Executable file
22
fast_api/app/utils/query_utils.py
Executable file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: query_utils.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: query-utility functions
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
|
||||
def to_dict(model, *args, exclude: List = None):
|
||||
q_dict = {}
|
||||
for c in model.__table__.columns:
|
||||
if not args or c.name in args:
|
||||
if not exclude or c.name not in exclude:
|
||||
q_dict[c.name] = getattr(model, c.name)
|
||||
|
||||
return q_dict
|
||||
2
fast_api/docker-build.sh
Normal file
2
fast_api/docker-build.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
docker build -t metaverse/medical_rest:latest .
|
||||
|
||||
18
fast_api/docker-compose.yml
Normal file
18
fast_api/docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: '2.1'
|
||||
services:
|
||||
api:
|
||||
# depends_on:
|
||||
# mysql:
|
||||
# condition: service_healthy
|
||||
container_name: metaverse_medical_rest
|
||||
image: metaverse/medical_rest:latest
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
volumes:
|
||||
- ./fast_api:/FAST_API
|
||||
ports:
|
||||
- 50510-50512:50510-50512
|
||||
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50510"] #local
|
||||
# command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50532"] #my
|
||||
10
fast_api/environment.yml
Executable file
10
fast_api/environment.yml
Executable file
@@ -0,0 +1,10 @@
|
||||
name: medical_metaverse
|
||||
|
||||
channels:
|
||||
- conda-forge
|
||||
|
||||
dependencies:
|
||||
- python=3.9
|
||||
- pip
|
||||
- pip:
|
||||
- -r requirements.txt
|
||||
222
fast_api/gunicorn.conf.py
Executable file
222
fast_api/gunicorn.conf.py
Executable file
@@ -0,0 +1,222 @@
|
||||
# Gunicorn configuration file.
|
||||
#
|
||||
# Server socket
|
||||
#
|
||||
# bind - The socket to bind.
|
||||
#
|
||||
# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
|
||||
# An IP is a valid HOST.
|
||||
#
|
||||
# backlog - The number of pending connections. This refers
|
||||
# to the number of clients that can be waiting to be
|
||||
# served. Exceeding this number results in the client
|
||||
# getting an error when attempting to connect. It should
|
||||
# only affect servers under significant load.
|
||||
#
|
||||
# Must be a positive integer. Generally set in the 64-2048
|
||||
# range.
|
||||
#
|
||||
|
||||
bind = "0.0.0.0:5000"
|
||||
backlog = 2048
|
||||
|
||||
#
|
||||
# Worker processes
|
||||
#
|
||||
# workers - The number of worker processes that this server
|
||||
# should keep alive for handling requests.
|
||||
#
|
||||
# A positive integer generally in the 2-4 x $(NUM_CORES)
|
||||
# range. You'll want to vary this a bit to find the best
|
||||
# for your particular application's work load.
|
||||
#
|
||||
# worker_class - The type of workers to use. The default
|
||||
# sync class should handle most 'normal' types of work
|
||||
# loads. You'll want to read
|
||||
# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
|
||||
# for information on when you might want to choose one
|
||||
# of the other worker classes.
|
||||
#
|
||||
# A string referring to a Python path to a subclass of
|
||||
# gunicorn.workers.base.Worker. The default provided values
|
||||
# can be seen at
|
||||
# http://docs.gunicorn.org/en/latest/settings.html#worker-class
|
||||
#
|
||||
# worker_connections - For the eventlet and gevent worker classes
|
||||
# this limits the maximum number of simultaneous clients that
|
||||
# a single process can handle.
|
||||
#
|
||||
# A positive integer generally set to around 1000.
|
||||
#
|
||||
# timeout - If a worker does not notify the master process in this
|
||||
# number of seconds it is killed and a new worker is spawned
|
||||
# to replace it.
|
||||
#
|
||||
# Generally set to thirty seconds. Only set this noticeably
|
||||
# higher if you're sure of the repercussions for sync workers.
|
||||
# For the non sync workers it just means that the worker
|
||||
# process is still communicating and is not tied to the length
|
||||
# of time required to handle a single request.
|
||||
#
|
||||
# keepalive - The number of seconds to wait for the next request
|
||||
# on a Keep-Alive HTTP connection.
|
||||
#
|
||||
# A positive integer. Generally set in the 1-5 seconds range.
|
||||
#
|
||||
# reload - Restart workers when code changes.
|
||||
#
|
||||
# This setting is intended for development. It will cause
|
||||
# workers to be restarted whenever application code changes.
|
||||
workers = 3
|
||||
threads = 3
|
||||
worker_class = "uvicorn.workers.UvicornWorker"
|
||||
worker_connections = 1000
|
||||
timeout = 60
|
||||
keepalive = 2
|
||||
reload = True
|
||||
|
||||
#
|
||||
# spew - Install a trace function that spews every line of Python
|
||||
# that is executed when running the server. This is the
|
||||
# nuclear option.
|
||||
#
|
||||
# True or False
|
||||
#
|
||||
|
||||
spew = False
|
||||
|
||||
#
|
||||
# Server mechanics
|
||||
#
|
||||
# daemon - Detach the main Gunicorn process from the controlling
|
||||
# terminal with a standard fork/fork sequence.
|
||||
#
|
||||
# True or False
|
||||
#
|
||||
# raw_env - Pass environment variables to the execution environment.
|
||||
#
|
||||
# pidfile - The path to a pid file to write
|
||||
#
|
||||
# A path string or None to not write a pid file.
|
||||
#
|
||||
# user - Switch worker processes to run as this user.
|
||||
#
|
||||
# A valid user id (as an integer) or the name of a user that
|
||||
# can be retrieved with a call to pwd.getpwnam(value) or None
|
||||
# to not change the worker process user.
|
||||
#
|
||||
# group - Switch worker process to run as this group.
|
||||
#
|
||||
# A valid group id (as an integer) or the name of a user that
|
||||
# can be retrieved with a call to pwd.getgrnam(value) or None
|
||||
# to change the worker processes group.
|
||||
#
|
||||
# umask - A mask for file permissions written by Gunicorn. Note that
|
||||
# this affects unix socket permissions.
|
||||
#
|
||||
# A valid value for the os.umask(mode) call or a string
|
||||
# compatible with int(value, 0) (0 means Python guesses
|
||||
# the base, so values like "0", "0xFF", "0022" are valid
|
||||
# for decimal, hex, and octal representations)
|
||||
#
|
||||
# tmp_upload_dir - A directory to store temporary request data when
|
||||
# requests are read. This will most likely be disappearing soon.
|
||||
#
|
||||
# A path to a directory where the process owner can write. Or
|
||||
# None to signal that Python should choose one on its own.
|
||||
#
|
||||
|
||||
daemon = False
|
||||
pidfile = None
|
||||
umask = 0
|
||||
user = None
|
||||
group = None
|
||||
tmp_upload_dir = None
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
# logfile - The path to a log file to write to.
|
||||
#
|
||||
# A path string. "-" means log to stdout.
|
||||
#
|
||||
# loglevel - The granularity of log output
|
||||
#
|
||||
# A string of "debug", "info", "warning", "error", "critical"
|
||||
#
|
||||
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
accesslog = None
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
#
|
||||
# Process naming
|
||||
#
|
||||
# proc_name - A base to use with setproctitle to change the way
|
||||
# that Gunicorn processes are reported in the system process
|
||||
# table. This affects things like 'ps' and 'top'. If you're
|
||||
# going to be running more than one instance of Gunicorn you'll
|
||||
# probably want to set a name to tell them apart. This requires
|
||||
# that you install the setproctitle module.
|
||||
#
|
||||
# A string or None to choose a default of something like 'gunicorn'.
|
||||
#
|
||||
|
||||
proc_name = "NotificationAPI"
|
||||
|
||||
|
||||
#
|
||||
# Server hooks
|
||||
#
|
||||
# post_fork - Called just after a worker has been forked.
|
||||
#
|
||||
# A callable that takes a server and worker instance
|
||||
# as arguments.
|
||||
#
|
||||
# pre_fork - Called just prior to forking the worker subprocess.
|
||||
#
|
||||
# A callable that accepts the same arguments as after_fork
|
||||
#
|
||||
# pre_exec - Called just prior to forking off a secondary
|
||||
# master process during things like config reloading.
|
||||
#
|
||||
# A callable that takes a server instance as the sole argument.
|
||||
#
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
server.log.info("Worker spawned (pid: %s)", worker.pid)
|
||||
|
||||
|
||||
def pre_fork(server, worker):
|
||||
pass
|
||||
|
||||
|
||||
def pre_exec(server):
|
||||
server.log.info("Forked child, re-executing.")
|
||||
|
||||
|
||||
def when_ready(server):
|
||||
server.log.info("Server is ready. Spawning workers")
|
||||
|
||||
|
||||
def worker_int(worker):
|
||||
worker.log.info("worker received INT or QUIT signal")
|
||||
|
||||
# get traceback info
|
||||
import threading, sys, traceback
|
||||
|
||||
id2name = {th.ident: th.name for th in threading.enumerate()}
|
||||
code = []
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId))
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
|
||||
if line:
|
||||
code.append(" %s" % (line.strip()))
|
||||
worker.log.debug("\n".join(code))
|
||||
|
||||
|
||||
def worker_abort(worker):
|
||||
worker.log.info("worker received SIGABRT signal")
|
||||
14
fast_api/requirements.txt
Executable file
14
fast_api/requirements.txt
Executable file
@@ -0,0 +1,14 @@
|
||||
fastapi==0.70.1
|
||||
uvicorn==0.16.0
|
||||
pymysql==1.0.2
|
||||
sqlalchemy==1.4.29
|
||||
bcrypt==3.2.0
|
||||
pyjwt==2.3.0
|
||||
yagmail==0.14.261
|
||||
boto3==1.20.32
|
||||
pytest==6.2.5
|
||||
cryptography
|
||||
|
||||
#extra
|
||||
pycryptodomex
|
||||
pycryptodome
|
||||
18
fast_api/test_main.py
Executable file
18
fast_api/test_main.py
Executable file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: test_main.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: 개발용 실행
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
from app.common.config import conf
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('test_main.py run')
|
||||
uvicorn.run('app.main:app', host='0.0.0.0', port=conf().REST_SERVER_PORT, reload=True)
|
||||
0
fast_api/tests/__init__.py
Executable file
0
fast_api/tests/__init__.py
Executable file
79
fast_api/tests/conftest.py
Executable file
79
fast_api/tests/conftest.py
Executable file
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: conftest.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: test config
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from os import path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database.schema import Users
|
||||
from app.main import create_app
|
||||
from app.database.conn import db, Base
|
||||
from app.models import UserToken
|
||||
from app.routes.auth import create_access_token
|
||||
|
||||
|
||||
"""
|
||||
1. DB 생성
|
||||
2. 테이블 생성
|
||||
3. 테스트 코드 작동
|
||||
4. 테이블 레코드 삭제
|
||||
"""
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def app():
|
||||
os.environ['API_ENV'] = 'test'
|
||||
return create_app()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def client(app):
|
||||
# Create tables
|
||||
Base.metadata.create_all(db.engine)
|
||||
return TestClient(app=app)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def session():
|
||||
sess = next(db.session())
|
||||
yield sess
|
||||
clear_all_table_data(
|
||||
session=sess,
|
||||
metadata=Base.metadata,
|
||||
except_tables=[]
|
||||
)
|
||||
sess.rollback()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def login(session):
|
||||
"""
|
||||
테스트전 사용자 미리 등록
|
||||
:param session:
|
||||
:return:
|
||||
"""
|
||||
db_user = Users.create(session=session, email='ryan_test@dingrr.com', pw='123')
|
||||
session.commit()
|
||||
access_token = create_access_token(data=UserToken.from_orm(db_user).dict(exclude={'pw', 'marketing_agree'}),)
|
||||
return dict(Authorization=f'Bearer {access_token}')
|
||||
|
||||
|
||||
def clear_all_table_data(session: Session, metadata, except_tables: List[str] = None):
|
||||
session.execute('SET FOREIGN_KEY_CHECKS = 0;')
|
||||
for table in metadata.sorted_tables:
|
||||
if table.name not in except_tables:
|
||||
session.execute(table.delete())
|
||||
session.execute('SET FOREIGN_KEY_CHECKS = 1;')
|
||||
session.commit()
|
||||
44
fast_api/tests/test_auth.py
Executable file
44
fast_api/tests/test_auth.py
Executable file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: test_auth.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: test auth.
|
||||
"""
|
||||
|
||||
from app.database.conn import db
|
||||
from app.database.schema import Users
|
||||
|
||||
|
||||
def test_registration(client, session):
|
||||
"""
|
||||
레버 로그인
|
||||
:param client:
|
||||
:param session:
|
||||
:return:
|
||||
"""
|
||||
user = dict(email='ryan@dingrr.com', pw='123', name='라이언', phone='01099999999')
|
||||
res = client.post('api/auth/register/email', json=user)
|
||||
res_body = res.json()
|
||||
print(res.json())
|
||||
assert res.status_code == 201
|
||||
assert 'Authorization' in res_body.keys()
|
||||
|
||||
|
||||
def test_registration_exist_email(client, session):
|
||||
"""
|
||||
레버 로그인
|
||||
:param client:
|
||||
:param session:
|
||||
:return:
|
||||
"""
|
||||
user = dict(email='Hello@dingrr.com', pw='123', name='라이언', phone='01099999999')
|
||||
db_user = Users.create(session=session, **user)
|
||||
session.commit()
|
||||
res = client.post('api/auth/register/email', json=user)
|
||||
res_body = res.json()
|
||||
assert res.status_code == 400
|
||||
assert 'EMAIL_EXISTS' == res_body['msg']
|
||||
33
fast_api/tests/test_user.py
Executable file
33
fast_api/tests/test_user.py
Executable file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@File: test_user.py
|
||||
@Date: 2020-09-14
|
||||
@author: A2TEC
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
|
||||
@brief: test user
|
||||
"""
|
||||
|
||||
from app.database.conn import db
|
||||
from app.database.schema import Users
|
||||
|
||||
|
||||
def test_create_get_apikey(client, session, login):
|
||||
"""
|
||||
레버 로그인
|
||||
:param client:
|
||||
:param session:
|
||||
:return:
|
||||
"""
|
||||
key = dict(user_memo='ryan__key')
|
||||
res = client.post('api/user/apikeys', json=key, headers=login)
|
||||
res_body = res.json()
|
||||
assert res.status_code == 200
|
||||
assert 'secret_key' in res_body
|
||||
|
||||
res = client.get('api/user/apikeys', headers=login)
|
||||
res_body = res.json()
|
||||
assert res.status_code == 200
|
||||
assert 'ryan__key' in res_body[0]['user_memo']
|
||||
|
||||
27
readme.md
Normal file
27
readme.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# A2TEC_METAVERSE_MEDICAL_REST_SERVER
|
||||
|
||||
A2TEC_METAVERSE_MEDICAL_REST_SERVER
|
||||
RESTful API Server
|
||||
|
||||
|
||||
### System Requirements
|
||||
- Ubuntu 20.04
|
||||
- docker 20.10.17
|
||||
- docker-compose 1.29.2
|
||||
|
||||
|
||||
### Docker
|
||||
- Base Image
|
||||
* python:3.9.7-slim
|
||||
* mysql:latest
|
||||
|
||||
- Target Image
|
||||
* metaverse/medical_rest:latest
|
||||
|
||||
- Build & Run
|
||||
* docker-compose.yml 파일의 환경변수(environment) 수정
|
||||
* fast_api/app/common/consts.py 환경에 맞게 수정 후 실행
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user