diff --git a/README.md b/README.md index 915b472..a6d4490 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,6 @@ ```shell export PYTHONPATH=/src uvicorn gentrade-server.main:app --reload -``` \ No newline at end of file +or +python -m gentrade-server.main +``` diff --git a/requirements.txt b/requirements.txt index 0195b38..0dad6d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ fastapi[standard] -uvicorn \ No newline at end of file +uvicorn +pydantic +pydantic-settings +ntplib +python-dateutil diff --git a/src/gentrade-server/main.py b/src/gentrade-server/main.py deleted file mode 100644 index 9a67f98..0000000 --- a/src/gentrade-server/main.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Depends -from .routers import secure, public -from .auth import get_user - -app = FastAPI() - -app.include_router( - public.router, - prefix="/api/v1/public" -) -app.include_router( - secure.router, - prefix="/api/v1/secure", - dependencies=[Depends(get_user)] -) diff --git a/src/gentrade-server/routers/public.py b/src/gentrade-server/routers/public.py deleted file mode 100644 index 88373eb..0000000 --- a/src/gentrade-server/routers/public.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import APIRouter - - -router = APIRouter() - -@router.get("/") -async def get_testroute(): - return "OK" \ No newline at end of file diff --git a/src/gentrade-server/__init__.py b/src/gentrade_server/__init__.py similarity index 100% rename from src/gentrade-server/__init__.py rename to src/gentrade_server/__init__.py diff --git a/src/gentrade-server/auth.py b/src/gentrade_server/auth.py similarity index 62% rename from src/gentrade-server/auth.py rename to src/gentrade_server/auth.py index 70e6804..e20dc5c 100644 --- a/src/gentrade-server/auth.py +++ b/src/gentrade_server/auth.py @@ -1,12 +1,18 @@ +""" +Authorization module +""" from fastapi import Security, HTTPException, status from fastapi.security import APIKeyHeader from .db import check_api_key, get_user_from_api_key api_key_header = APIKeyHeader(name="X-API-Key") -def get_user(api_key_header: str = Security(api_key_header)): - if check_api_key(api_key_header): - user = get_user_from_api_key(api_key_header) +def get_user(header: str = Security(api_key_header)): + """ + Get user from the API key + """ + if check_api_key(header): + user = get_user_from_api_key(header) return user raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/src/gentrade_server/config.py b/src/gentrade_server/config.py new file mode 100644 index 0000000..9523d06 --- /dev/null +++ b/src/gentrade_server/config.py @@ -0,0 +1,12 @@ +""" +Configure +""" +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + """ + Settings + """ + ntp_server: str = "ntp.ntsc.ac.cn" + +settings = Settings() diff --git a/src/gentrade-server/db.py b/src/gentrade_server/db.py similarity index 78% rename from src/gentrade-server/db.py rename to src/gentrade_server/db.py index 7266790..4d14754 100644 --- a/src/gentrade-server/db.py +++ b/src/gentrade_server/db.py @@ -1,3 +1,6 @@ +""" +Database +""" api_keys = { "e54d4431-5dab-474e-b71a-0db1fcb9e659": "7oDYjo3d9r58EJKYi5x4E8", "5f0c7127-3be9-4488-b801-c7b6415b45e9": "mUP7PpTHmFAkxcQLWKMY8t" @@ -13,7 +16,13 @@ } def check_api_key(api_key: str): + """ + Check whether API key valid + """ return api_key in api_keys def get_user_from_api_key(api_key: str): + """ + Get the user from the given API key + """ return users[api_keys[api_key]] diff --git a/src/gentrade_server/main.py b/src/gentrade_server/main.py new file mode 100644 index 0000000..3d9f134 --- /dev/null +++ b/src/gentrade_server/main.py @@ -0,0 +1,49 @@ +""" +The main entry +""" +import logging +from contextlib import asynccontextmanager + +import uvicorn +from fastapi import FastAPI, Depends +from fastapi.middleware.cors import CORSMiddleware + +from .routers import secure, public +from .auth import get_user +from .util import sync_ntp_server + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') +LOG = logging.getLogger(__name__) + +@asynccontextmanager +async def lifespan(_:FastAPI): + """ + App lifecycle + """ + LOG.info("Starting Up...") + sync_ntp_server() + yield + LOG.info("Shutting Down...") + +app = FastAPI() +app = FastAPI(lifespan=lifespan) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router( + public.router, + prefix="/api/v1/public" +) +app.include_router( + secure.router, + prefix="/api/v1/secure", + dependencies=[Depends(get_user)] +) + +if __name__ == '__main__': + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/gentrade-server/routers/__init__.py b/src/gentrade_server/routers/__init__.py similarity index 100% rename from src/gentrade-server/routers/__init__.py rename to src/gentrade_server/routers/__init__.py diff --git a/src/gentrade_server/routers/public.py b/src/gentrade_server/routers/public.py new file mode 100644 index 0000000..9cc10dd --- /dev/null +++ b/src/gentrade_server/routers/public.py @@ -0,0 +1,65 @@ +""" +Public result API interfaces +""" +import logging + +import time +import datetime +from dateutil.tz import tzlocal + +from fastapi import APIRouter +from pydantic import BaseModel + +from ..util import sync_ntp_server +from ..config import settings + +LOG = logging.getLogger(__name__) + +router = APIRouter() + +@router.get("/") +async def get_testroute(): + """ + Test public interface + """ + return "OK" + +class HealthCheck(BaseModel): + """ + Response model to validate and return when performing a health check. + """ + + status: str = "OK" + +@router.get("/health") +async def get_health() -> HealthCheck: + """ + Check health + """ + return HealthCheck(status="OK") + +@router.get("/settings") +async def get_settings(): + """ + Get server settings + """ + return { + 'ntp_server': settings.ntp_server + } + +@router.get("/server_time") +async def get_server_time(): + """ + Get server time + """ + curr_ts = time.time() + now_utc = datetime.datetime.fromtimestamp(curr_ts, datetime.UTC) + tl = tzlocal() + ntp_offset = sync_ntp_server() + + return { + 'ntp_offset': ntp_offset, + 'timezone_name': tl.tzname(now_utc), + 'timezone_offset': tl.utcoffset(now_utc).total_seconds(), + 'timestamp_server': int(curr_ts) + } diff --git a/src/gentrade-server/routers/secure.py b/src/gentrade_server/routers/secure.py similarity index 65% rename from src/gentrade-server/routers/secure.py rename to src/gentrade_server/routers/secure.py index 4633323..d47347a 100644 --- a/src/gentrade-server/routers/secure.py +++ b/src/gentrade_server/routers/secure.py @@ -1,3 +1,6 @@ +""" +Secure API interface +""" from fastapi import APIRouter, Depends from ..auth import get_user @@ -5,4 +8,7 @@ @router.get("/") async def get_testroute(user: dict = Depends(get_user)): - return user \ No newline at end of file + """ + Test secure interface + """ + return user diff --git a/src/gentrade_server/util.py b/src/gentrade_server/util.py new file mode 100644 index 0000000..913fa67 --- /dev/null +++ b/src/gentrade_server/util.py @@ -0,0 +1,32 @@ +""" +Utils +""" +import logging +import ntplib + +LOG = logging.getLogger(__name__) + +def sync_ntp_server() -> float: + """ + Sync with NTP server + + :return : offset in seconds + """ + ntp_servers = ['ntp.ntsc.ac.cn', 'ntp.sjtu.edu.cn', 'cn.ntp.org.cn', + 'cn.pool.ntp.org', 'ntp.aliyun.com'] + retry = len(ntp_servers) - 1 + client = ntplib.NTPClient() + while retry > 0: + LOG.info("Try to get time from NTP: %s", ntp_servers[retry]) + + try: + ret = client.request(ntp_servers[retry], version=3) + offset = (ret.recv_time - ret.orig_time + + ret.dest_time - ret.tx_time) / 2 + LOG.info("NTP offset: %.2f", offset) + return offset + except ntplib.NTPException: + LOG.error("Fail to get time, try another") + retry -= 1 + continue + return None