diff --git a/.env b/.env deleted file mode 100644 index 48c7132ea373b06ddfcebb2a0fa90905f43cbdc7..0000000000000000000000000000000000000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -API_BASE_URL=https://api-dev.futabus.vn \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2eea525d885d5148108f6f3a9a8613863f783d36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 43ea96c4fba11df8a88305829c46209ace470383..6c39f59e7042f3c910a6fa7069e597f79033013f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ -FROM python:3.9 +FROM python:3.10.4 -ENV API_BASE_URL https://api-dev.futabus.vn +ENV API_BASE_URL 'https://api-dev.futabus.vn' +ENV API_ACCESS_TOKEN 'https://api-dev.futabus.vn/identity/api/token/anonymous-token' +ENV ENVIRONMENT 'dev' COPY . . diff --git a/__pycache__/app.cpython-39.pyc b/__pycache__/app.cpython-39.pyc deleted file mode 100644 index a43c4660862fb5c5c83da69c2308c9d1bfb81884..0000000000000000000000000000000000000000 Binary files a/__pycache__/app.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/main.cpython-39.pyc b/__pycache__/main.cpython-39.pyc deleted file mode 100644 index 673e5341041ddac59633012030e2cf9f41e6e669..0000000000000000000000000000000000000000 Binary files a/__pycache__/main.cpython-39.pyc and /dev/null differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c70d6fec18121ddccee53d267dfe03b0f96dc121 Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/router.cpython-310.pyc b/app/__pycache__/router.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..360e484f2063c410af1987e8bab82d659664cb41 Binary files /dev/null and b/app/__pycache__/router.cpython-310.pyc differ diff --git a/app/busbooking-451909-aa92e337868d.json b/app/busbooking-451909-aa92e337868d.json deleted file mode 100644 index cec34b5b05acdc649f0446760e6012520c308e85..0000000000000000000000000000000000000000 --- a/app/busbooking-451909-aa92e337868d.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "busbooking-451909", - "private_key_id": "aa92e337868d77340f1e1bc3d0119738f48b8a49", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwozPFZx+v2DH+\nXFMLaft+4tnaWKPDGEgQ0IGwmXUtgVzd7iuH/fI9tXFiRw1ecFGAzundf1qiJJis\npMgfMxXm5fWpaKa4AM5LKxootKYPLZeJ3/N+BhJkCSmDXZmk6TuBff3WBRUkDj9n\nQTnpRSCBsCxArvm2zw6PSUqHwPiW/g05q6escmHXZUO3wpLgFz41u1Xy7M2Rc/bO\n3j6OYogYxZkp5aY5EpWhUKO2+qNVaVQqDweJhQ7/M+UpX5Va9VdMAM7dm3BUIy1Y\n3c8NDgUvL5uXKljoHrPpGXm+1QZTnrT18y3Jmo5PxkIF0u/SXDnFTYl9CL876ewq\nYaN55d9fAgMBAAECggEAArpm8Nr9cfnqfy6+xkdaUZLy01Xj7WdOEdq7Taw/ttdb\nnSyBE9aeM3LmKS4TCboOQn6WCivSdDoj/PkVR71Fh0ueIGCOW1GvBQ0lC8cYht2G\ndUqzsP8SoE22ScX64vK9+PbbtNxz4+fBckM8C9f7yVyc89LIA/mO+bLkBGv8pYGQ\nNg2Po8Oi8wjA06XMEaFVfrURnyQoUVVeIXBn/L0TViL/vSo0wshYQw9hoXI6RrUW\n1SDjfRmzOIt63R7x4Bn/BZYvgjtRewAL325ooZVXqrzXHZ9yinC/7BIrxQCF9Ncr\nuKok9SDhNES2OUUbO8QlwW4EVvTkiCb59r26jqU26QKBgQD6K2AyZ1u2XRI9HpW3\nsAeshUDcG/Gj0cnWQlpe6hr+UOrMt7LIgOhuQeR1/5EZ/ZLG8FYKE096ffP/2ND8\nG+JzTTj5Fiv9ZzUdKWPQGE5D6TwJmVAO6R8ZaodykkIgg7Thn17og4EbeSAqNtey\nh2LSLnYrT9imjfR78n+vPYCwfQKBgQD2PvRknpN+zMYnMUA/pho0/c7G8kbORiWp\nDpHabHDbtdKSgWC3enQ3bVPSeGiZNvrjSar+1/8H2C3WovhgYTp4o7+IdM2cKWOZ\nJsQNbgoXYgeaylIiwZGRQN0x1q4ho2w3UGxu3spupyFFo8yNRay8JkCI6OMX+ZWd\nG2QXQEOSCwKBgQCrZa55uhC+x9NoJp1DBYqsa3t9knOi3mffsQRDhTdLSFsmOTF3\nZ8JXUDPbmGZsnSvDuwPn0UUh0kuq3XyJTf1/K8g9+C/ZZK2iNipZd12f75sfpHeS\nT6vr+O2l1IkTx8jU0CDxQq/hB8K+yWZMva85+3UgxYrUyetYRFOw131k7QKBgGxc\nt9+viOi75FdK7SMVTWMUbfJOm6oaZGhI6RZdsix9jvS5yn3zfUEG82QjaKRD9ZQf\nzwfmtWwWTdWuUe7X2otMQ/UgsXqPHC1BSfU+/2Ha2c3cStjQpeZtzOkpt+dFq1GM\nKqt/j0WydonW0yU4DBOgIbYeBhF+28APVbSFqzaRAoGBAPRPNNCJcbIsCZEyBgwn\nCgK9W5anZDVmtwXbAVAoKMzzxFVi49W+6Lduw9oeTqgx9J1AdQMwV27I0hcPtfBE\nw5IVhrQsd6dRKDt/CJwEN6/iR9AqLx3T/rO342yOIi9GSPsFISQiiPDywfkNSzj6\nhT0eDw3eRs/RUv0zqcjFUjNI\n-----END PRIVATE KEY-----\n", - "client_email": "chatbot@busbooking-451909.iam.gserviceaccount.com", - "client_id": "111019976759266480456", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/chatbot%40busbooking-451909.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/app/core/config.py b/app/core/config.py deleted file mode 100644 index a28112daf2eaf7de4f73a5a52754db3b9531c21a..0000000000000000000000000000000000000000 --- a/app/core/config.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -from dotenv import load_dotenv - -load_dotenv() # Load biến môi trường từ file .env - -class Settings: - API_BASE_URL = os.getenv("API_BASE_URL") # API gốc (backend dev) - -settings = Settings() diff --git a/app/dialogflow/api/__init__.py b/app/dialogflow/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/dialogflow/api/__pycache__/__init__.cpython-310.pyc b/app/dialogflow/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab00aa5273d1aae6fa53979d54ab85d2f61930de Binary files /dev/null and b/app/dialogflow/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/dialogflow/api/__pycache__/router.cpython-310.pyc b/app/dialogflow/api/__pycache__/router.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3660678dc89f08d7577975da9c77488ca5a11a80 Binary files /dev/null and b/app/dialogflow/api/__pycache__/router.cpython-310.pyc differ diff --git a/app/api/__pycache__/routes.cpython-39.pyc b/app/dialogflow/api/__pycache__/routes.cpython-39.pyc similarity index 100% rename from app/api/__pycache__/routes.cpython-39.pyc rename to app/dialogflow/api/__pycache__/routes.cpython-39.pyc diff --git a/app/dialogflow/api/router.py b/app/dialogflow/api/router.py new file mode 100644 index 0000000000000000000000000000000000000000..723cfbfc94e3e6e255a99d0bb291336359f9d90b --- /dev/null +++ b/app/dialogflow/api/router.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from app.dialogflow.api.v1.dialogflow import router as dialogflow_router +from core.conf import settings + +v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH) + +v1.include_router(dialogflow_router) \ No newline at end of file diff --git a/app/dialogflow/api/v1/__init__.py b/app/dialogflow/api/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc b/app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c76d75cfca479b2255728f004779f13fbbcaa589 Binary files /dev/null and b/app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc b/app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..550eff59db74f5adcebb809479160a3e08360c55 Binary files /dev/null and b/app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc differ diff --git a/app/api/routes.py b/app/dialogflow/api/v1/dialogflow.py similarity index 79% rename from app/api/routes.py rename to app/dialogflow/api/v1/dialogflow.py index 1221f61443aa333b6ba813f282849e7bfa76d35c..e75586b61dce29a6e011f7111d21f51cb16cbc7f 100644 --- a/app/api/routes.py +++ b/app/dialogflow/api/v1/dialogflow.py @@ -3,67 +3,34 @@ from fastapi import FastAPI, APIRouter, HTTPException, Request, Response # type: from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse # type: ignore from datetime import datetime, timedelta from fastapi.templating import Jinja2Templates +from app.dialogflow.services.dialog_service import dialog_service -from app.services import api -from app.utils.constants import code_province -from app.types.Respone import DialogFlowResponseAPI +from common.external.external_api import api +from app.dialogflow.services.origin_codes import origin_codes +from common.response.respone_dialogflow import DialogFlowResponseAPI router = APIRouter() -templates = Jinja2Templates(directory="app/templates") - -def to_datetime_from_Dialogflow(time: dict): - date_time = datetime(int(time["year"]), int(time["month"]), int(time["day"])) - return date_time - -def process_dates_to_timestamp(from_time: datetime = None, to_time: datetime = None): - if to_time is None and from_time is not None: - to_time = from_time.replace(hour=23, minute=59, second=59) - - if from_time is None: - today = datetime.today().date() - from_time = datetime.combine(today, datetime.min.time()) - to_time = datetime.combine(today, datetime.max.time()) - timedelta(microseconds=1) - - return int(from_time.timestamp()) * 1000 , int(to_time.timestamp()) * 1000 -def get_param_from_dialogflow(body: any): - session_info = body.get("sessionInfo", {}) - parameters = session_info.get("parameters", {}) if isinstance(session_info.get("parameters"), dict) else {} - raw_date = parameters.get("date") - if raw_date is not None: - raw_date = to_datetime_from_Dialogflow(raw_date) - raw_departure_city = parameters.get("departure_city") - raw_destination_city = parameters.get("destination_city") - raw_ticket_number = parameters.get("ticket_number") - raw_time_of_day = parameters.get("time_of_day") - return raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day - -async def search_route_ids_from_province(departure_code: str, destination_code: str): - response = await api.get(f'/metadata/office/routes?DestCode={destination_code}&OriginCode={departure_code}') - route_ids = [] - if isinstance(response, list): # Kiểm tra nếu data là danh sách - route_ids = [route.get("routeId", -1) for route in response] - - return route_ids +templates = Jinja2Templates(directory="templates") @router.post('/routes') async def route(request: Request): body = await request.json() - raw_departure_city, raw_destination_city, raw_ticket_number , raw_date, _ = get_param_from_dialogflow(body) + raw_departure_city, raw_destination_city, raw_ticket_number , raw_date, _ = dialog_service.get_param_from_dialogflow(body) ticket_count = int(raw_ticket_number) if raw_ticket_number else 1 if raw_date is None: - from_time, to_time = process_dates_to_timestamp() + from_time, to_time = dialog_service.process_dates_to_timestamp() date = datetime.today().date().strftime('%m-%d-%Y') else: date = raw_date.strftime('%m-%d-%Y') - from_time, to_time = process_dates_to_timestamp(raw_date) - departure_code = code_province.get(raw_departure_city) - destination_code = code_province.get(raw_destination_city) - route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code) - route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code) + from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date) + departure_code = origin_codes.get(raw_departure_city) + destination_code = origin_codes.get(raw_destination_city) + route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code) + route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code) routes_ids = list(set(route_dep_to_des + route_des_to_dep)) payload = { "from_time": from_time, @@ -128,16 +95,16 @@ async def route(request: Request): @router.post('/price') async def price(request: Request): body = await request.json() - raw_departure_city, raw_destination_city, _, raw_date, _ = get_param_from_dialogflow(body) + raw_departure_city, raw_destination_city, _, raw_date, _ = dialog_service.get_param_from_dialogflow(body) if raw_date is None: - from_time, to_time = process_dates_to_timestamp() - from_time, to_time = process_dates_to_timestamp(raw_date) + from_time, to_time = dialog_service.process_dates_to_timestamp() + from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date) - departure_code = code_province.get(raw_departure_city) - destination_code = code_province.get(raw_destination_city) - route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code) - route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code) + departure_code = origin_codes.get(raw_departure_city) + destination_code = origin_codes.get(raw_destination_city) + route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code) + route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code) routes_ids = list(set(route_dep_to_des + route_des_to_dep)) payload = { "from_time": from_time, @@ -193,17 +160,17 @@ async def price(request: Request): @router.post('/trip/list') async def booking_trip(request: Request) -> Response: body = await request.json() - raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day = get_param_from_dialogflow(body) + raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day = dialog_service.get_param_from_dialogflow(body) date = raw_date.strftime('%m-%d-%Y') - from_time, to_time = process_dates_to_timestamp(raw_date) + from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date) ticket_count = int(raw_ticket_number) if raw_ticket_number else 1 - departure_code = code_province.get(raw_departure_city) - destination_code = code_province.get(raw_destination_city) + departure_code = origin_codes.get(raw_departure_city) + destination_code = origin_codes.get(raw_destination_city) - route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code) - route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code) + route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code) + route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code) routes_ids = list(set(route_dep_to_des + route_des_to_dep)) payload = { "from_time": from_time, @@ -233,7 +200,9 @@ async def booking_trip(request: Request) -> Response: "id": trip["id"], "departure_date": trip["raw_departure_date"], "departure_time": trip["raw_departure_time"], - "kind": trip["seat_type_name"] + "kind": trip["seat_type_name"], + "way_id": trip["way_id"] + }) text = ["Quý khách vui lòng lựa chọn chuyến xe\n" + "\n".join(f"{i+1}. {name}" for i, name in enumerate(routes_name))] payload={ @@ -301,13 +270,16 @@ async def is_valid_select_trip(request: Request) -> Response: if trip["route_name"] == raw_input: route_id = int(trip["route_id"]) kind = trip["kind"] + way_id = trip["way_id"] break parameters = { "is_valid_trip": True, "route_name": raw_input, "kind": kind, - "route_id": route_id + "route_id": route_id, + "way_id": way_id, + } else: parameters = { @@ -333,7 +305,7 @@ async def time_trip(request: Request) -> Response: parameters = { "time_list": time_list } - text = [f"Quý khách lựa chọn thời gian chuyến xe {route_name}\n" + " | ".join(map(str, time_list))] + text = [f"Quý khách lựa chọn thời gian chuyến {route_name}\n" + " | ".join(map(str, time_list))] payload={ "richContent": [ [ @@ -368,7 +340,7 @@ async def is_valid_select_time(request: Request) -> Response: "is_valid_time": True, "departure_time": raw_input } - text = [f'{raw_input} | {route_name}'] + text = [f' Quý khách chọn chuyến {raw_input} | {route_name}'] else: parameters = { "is_valid_time": False @@ -379,6 +351,40 @@ async def is_valid_select_time(request: Request) -> Response: print(e) return DialogFlowResponseAPI(text=["Hệ thống xảy ra lỗi. Quý khách vui lòng thử lại sau hoặc liên hệ Trung tâm tổng đài 1900 6067 để được hỗ trợ."]) + +@router.post('/trip/seats') +async def seats_trip(request: Request) -> Response: + try: + body = await request.json() + session_info = body.get("sessionInfo", {}) + parameters = session_info.get("parameters") + + trip_id: int = parameters.get("trip_id", None) + route_id: int = parameters.get("route_id", None) + departure_date: str = parameters.get("departure_date", None) + departure_time: str = parameters.get("departure_time", None) + kind: str = parameters.get("kind", None) + seats_empty, seats = await dialog_service.seats_trip(route_id, trip_id, departure_date, departure_time, kind) + text=["Vui lòng chọn ghế"] + payload={ + "richContent": [ + [ + { + "type": "chips", + "options": [ + {"text": seat["chair"]} for seat in (seats_empty) + ] + } + ] + ] + } + parameters = { + "seats": seats + } + return DialogFlowResponseAPI(text=text, payload=payload, parameters=parameters) + except Exception as e: + print(e) + return DialogFlowResponseAPI(text=["Hệ thống xảy ra lỗi. Quý khách vui lòng thử lại sau hoặc liên hệ Trung tâm tổng đài 1900 6067 để được hỗ trợ."]) @router.get("/") def home(): return "Hello World!" diff --git a/app/dialogflow/schemas/__init__.py b/app/dialogflow/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/dialogflow/services/__init__.py b/app/dialogflow/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/dialogflow/services/__pycache__/__init__.cpython-310.pyc b/app/dialogflow/services/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ce9ff85240f76bd15cc6475ed821bbd6e877215 Binary files /dev/null and b/app/dialogflow/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/services/__pycache__/api.cpython-39.pyc b/app/dialogflow/services/__pycache__/api.cpython-39.pyc similarity index 100% rename from app/services/__pycache__/api.cpython-39.pyc rename to app/dialogflow/services/__pycache__/api.cpython-39.pyc diff --git a/app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc b/app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c35f7aefb27484fe06bccc4f67d5924300048a79 Binary files /dev/null and b/app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc differ diff --git a/app/services/__pycache__/external_api.cpython-39.pyc b/app/dialogflow/services/__pycache__/external_api.cpython-39.pyc similarity index 100% rename from app/services/__pycache__/external_api.cpython-39.pyc rename to app/dialogflow/services/__pycache__/external_api.cpython-39.pyc diff --git a/app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc b/app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35a98c427220a47fcae789d37e411bb3c8791711 Binary files /dev/null and b/app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc differ diff --git a/app/dialogflow/services/dialog_service.py b/app/dialogflow/services/dialog_service.py new file mode 100644 index 0000000000000000000000000000000000000000..8b1b1657076808a652028b1e613e9d622e5a98a5 --- /dev/null +++ b/app/dialogflow/services/dialog_service.py @@ -0,0 +1,57 @@ +from datetime import datetime + +from common.external.external_api import api + +class DialogService: + @staticmethod + def to_datetime_from_Dialogflow(time: dict): + date_time = datetime(int(time["year"]), int(time["month"]), int(time["day"])) + return date_time + + def process_dates_to_timestamp(from_time: datetime = None, to_time: datetime = None): + if to_time is None and from_time is not None: + to_time = from_time.replace(hour=23, minute=59, second=59) + + if from_time is None: + today = datetime.today().date() + from_time = datetime.combine(today, datetime.min.time()) + to_time = datetime.combine(today, datetime.max.time()) - timedelta(microseconds=1) + + return int(from_time.timestamp()) * 1000 , int(to_time.timestamp()) * 1000 + def get_param_from_dialogflow(self, body: any): + session_info = body.get("sessionInfo", {}) + parameters = session_info.get("parameters", {}) if isinstance(session_info.get("parameters"), dict) else {} + raw_date = parameters.get("date") + if raw_date is not None: + raw_date = self.to_datetime_from_Dialogflow(raw_date) + raw_departure_city = parameters.get("departure_city") + raw_destination_city = parameters.get("destination_city") + raw_ticket_number = parameters.get("ticket_number") + raw_time_of_day = parameters.get("time_of_day") + return raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day + + async def search_route_ids_from_province(departure_code: str, destination_code: str): + response = await api.get(f'/metadata/office/routes?DestCode={destination_code}&OriginCode={departure_code}') + route_ids = [] + if isinstance(response, list): + route_ids = [route.get("routeId", -1) for route in response] + return route_ids + + + async def seats_trip(route_id: int, trip_id:int, departure_date: str, departure_time: str, kind: str): + try: + params = { + "departureDate": departure_date, + "departureTime": departure_time, + "kind": kind, + } + response = api.get(api_base="https://api-busline-dev.vato.vn/api", endpoint=f"/buslines/futa/booking/seats/{route_id}/{trip_id}" , params=params) + seats = response["data"] + seats_empty = [ seat for seat in seats if seat["bookStatus"] == 0 ] + return seats_empty, seats + except Exception as e: + print(e) + raise Exception("Error fetching seats data") + +dialog_service: DialogService = DialogService() + \ No newline at end of file diff --git a/app/dialogflow/services/origin_codes.py b/app/dialogflow/services/origin_codes.py new file mode 100644 index 0000000000000000000000000000000000000000..9dfd9ad39fccd30dd688d1af36646c25b5f68f50 --- /dev/null +++ b/app/dialogflow/services/origin_codes.py @@ -0,0 +1,4 @@ +import json + +with open("static/files/code_province.json", "r", encoding="utf-8") as file: + origin_codes = json.load(file) \ No newline at end of file diff --git a/app/router.py b/app/router.py new file mode 100644 index 0000000000000000000000000000000000000000..3ae08526bcfed0c4f710a370a7ff673062e9e074 --- /dev/null +++ b/app/router.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from app.dialogflow.api.router import v1 as dialogflow_v1 + +router = APIRouter() + +router.include_router(dialogflow_v1) diff --git a/app/services/api.py b/app/services/api.py deleted file mode 100644 index 869c7103ba25fe8c8a4774d89801ec1d93b0d75e..0000000000000000000000000000000000000000 --- a/app/services/api.py +++ /dev/null @@ -1,61 +0,0 @@ -import httpx -from app.core.config import settings -from typing import Optional, Dict, Any - -API_BASE_URL = settings.API_BASE_URL - -async def get(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None): - """Gọi API GET""" - url = f"{API_BASE_URL}{endpoint}" - headers = headers or {} - async with httpx.AsyncClient() as client: - try: - response = await client.get(url, headers=headers, params=params) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as http_err: - return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} - except Exception as err: - return {"error": f"Request failed: {str(err)}"} - -async def post(endpoint: str , payload: Dict[str, Any] = None,headers: Optional[Dict[str, str]] = None): - """Gọi API POST""" - url = f"{API_BASE_URL}{endpoint}" - headers = headers or {"Content-Type": "application/json"} - async with httpx.AsyncClient() as client: - try: - response = await client.post(url, json=payload, headers=headers) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as http_err: - return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} - except Exception as err: - return {"error": f"Request failed: {str(err)}"} - -async def put_api(endpoint: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None): - """Gọi API PUT""" - url = f"{API_BASE_URL}{endpoint}" - headers = headers or {"Content-Type": "application/json"} - async with httpx.AsyncClient() as client: - try: - response = await client.put(url, json=payload, headers=headers) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as http_err: - return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} - except Exception as err: - return {"error": f"Request failed: {str(err)}"} - -async def delete_api(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None): - """Gọi API DELETE""" - url = f"{API_BASE_URL}{endpoint}" - headers = headers or {} - async with httpx.AsyncClient() as client: - try: - response = await client.delete(url, headers=headers, params=params) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as http_err: - return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} - except Exception as err: - return {"error": f"Request failed: {str(err)}"} diff --git a/app/types/__pycache__/Respone.cpython-39.pyc b/app/types/__pycache__/Respone.cpython-39.pyc deleted file mode 100644 index 3d9e261cb2ef9413a673e87942874c223f16d64f..0000000000000000000000000000000000000000 Binary files a/app/types/__pycache__/Respone.cpython-39.pyc and /dev/null differ diff --git a/app/utils/__pycache__/constants.cpython-39.pyc b/app/utils/__pycache__/constants.cpython-39.pyc deleted file mode 100644 index 128d5fa5d0dfe6290b0d8d9371232003ac8586f8..0000000000000000000000000000000000000000 Binary files a/app/utils/__pycache__/constants.cpython-39.pyc and /dev/null differ diff --git a/app/utils/constants.py b/app/utils/constants.py deleted file mode 100644 index d99cc709bc72552254508bd45519f57be2f8cadc..0000000000000000000000000000000000000000 --- a/app/utils/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -import json - -# Đọc dữ liệu từ file JSON -with open("app/utils/code_province.json", "r", encoding="utf-8") as file: - code_province = json.load(file) \ No newline at end of file diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/__pycache__/__init__.cpython-310.pyc b/common/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..107b356cd2fc8574843e3cede276fb812bfdd13a Binary files /dev/null and b/common/__pycache__/__init__.cpython-310.pyc differ diff --git a/common/__pycache__/__init__.cpython-39.pyc b/common/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78fad8c50adab63259c295bd68116e94757e5777 Binary files /dev/null and b/common/__pycache__/__init__.cpython-39.pyc differ diff --git a/common/__pycache__/log.cpython-310.pyc b/common/__pycache__/log.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12e99aeb53b19b7eac30e3bc74a21ea223721482 Binary files /dev/null and b/common/__pycache__/log.cpython-310.pyc differ diff --git a/common/__pycache__/schema.cpython-310.pyc b/common/__pycache__/schema.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5527fe378b6f1d087bb58723742eeb2fe7c52f74 Binary files /dev/null and b/common/__pycache__/schema.cpython-310.pyc differ diff --git a/common/exception/__init__.py b/common/exception/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..56fafa58b3f43decb7699b93048b8b87e0f695aa --- /dev/null +++ b/common/exception/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/common/exception/__pycache__/__init__.cpython-310.pyc b/common/exception/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..571d01b5866436d845164d82ef70305d95396989 Binary files /dev/null and b/common/exception/__pycache__/__init__.cpython-310.pyc differ diff --git a/common/exception/__pycache__/__init__.cpython-39.pyc b/common/exception/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2496e6c4fe2211ccc5fffc6e335a7ac4fe9a15e Binary files /dev/null and b/common/exception/__pycache__/__init__.cpython-39.pyc differ diff --git a/common/exception/__pycache__/errors.cpython-310.pyc b/common/exception/__pycache__/errors.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac13ba2d13fd23c306da5249dfd5cbf1098a6b65 Binary files /dev/null and b/common/exception/__pycache__/errors.cpython-310.pyc differ diff --git a/common/exception/__pycache__/errors.cpython-39.pyc b/common/exception/__pycache__/errors.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f0597483d47f5cc3caf06a28aef7d6034ef09b3 Binary files /dev/null and b/common/exception/__pycache__/errors.cpython-39.pyc differ diff --git a/common/exception/__pycache__/exception_handler.cpython-310.pyc b/common/exception/__pycache__/exception_handler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cf398933c6e28f5defd42c8f43e3aa32501e4a2 Binary files /dev/null and b/common/exception/__pycache__/exception_handler.cpython-310.pyc differ diff --git a/common/exception/__pycache__/exception_handler.cpython-39.pyc b/common/exception/__pycache__/exception_handler.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bf49bf73d9969867e97347de4f6acdce63eb65b Binary files /dev/null and b/common/exception/__pycache__/exception_handler.cpython-39.pyc differ diff --git a/common/exception/errors.py b/common/exception/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..4dca3c800b77c56f7e1fbb3b16f67d75d3f5031c --- /dev/null +++ b/common/exception/errors.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Any + +from fastapi import HTTPException +from starlette.background import BackgroundTask + +from common.response.response_code import CustomErrorCode, StandardResponseCode + + +class BaseExceptionMixin(Exception): + + code: int + + def __init__(self, *, msg: str = None, data: Any = None, background: BackgroundTask | None = None): + self.msg = msg + self.data = data + # The original background task: https://www.starlette.io/background/ + self.background = background + + +class HTTPError(HTTPException): + """HTTP """ + + def __init__(self, *, code: int, msg: Any = None, headers: dict[str, Any] | None = None): + super().__init__(status_code=code, detail=msg, headers=headers) + + +class CustomError(BaseExceptionMixin): + + def __init__(self, *, error: CustomErrorCode, data: Any = None, background: BackgroundTask | None = None): + self.code = error.code + super().__init__(msg=error.msg, data=data, background=background) + + +class RequestError(BaseExceptionMixin): + code = StandardResponseCode.HTTP_400 + + def __init__(self, *, msg: str = 'Bad Request', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class ForbiddenError(BaseExceptionMixin): + + code = StandardResponseCode.HTTP_403 + + def __init__(self, *, msg: str = 'Forbidden', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class NotFoundError(BaseExceptionMixin): + code = StandardResponseCode.HTTP_404 + + def __init__(self, *, msg: str = 'Not Found', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class ServerError(BaseExceptionMixin): + + code = StandardResponseCode.HTTP_500 + + def __init__( + self, *, msg: str = 'Internal Server Error', data: Any = None, background: BackgroundTask | None = None + ): + super().__init__(msg=msg, data=data, background=background) + + +class GatewayError(BaseExceptionMixin): + + code = StandardResponseCode.HTTP_502 + + def __init__(self, *, msg: str = 'Bad Gateway', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class AuthorizationError(BaseExceptionMixin): + + code = StandardResponseCode.HTTP_401 + + def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class TokenError(HTTPError): + + code = StandardResponseCode.HTTP_401 + + def __init__(self, *, msg: str = 'Not Authenticated', headers: dict[str, Any] | None = None): + super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'}) diff --git a/common/exception/exception_handler.py b/common/exception/exception_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..a94552121d1d709e6a06c0185e3fb54b44a49f4e --- /dev/null +++ b/common/exception/exception_handler.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError +from starlette.exceptions import HTTPException +from starlette.middleware.cors import CORSMiddleware +from uvicorn.protocols.http.h11_impl import STATUS_PHRASES + +from common.exception.errors import BaseExceptionMixin +from common.response.response_code import CustomResponseCode, StandardResponseCode +from common.response.response_schema import response_base +from common.schema import ( + CUSTOM_VALIDATION_ERROR_MESSAGES, +) +from core.conf import settings +from utils.serializers import MsgSpecJSONResponse +from utils.trace_id import get_request_trace_id + + +def _get_exception_code(status_code: int) -> int: + try: + STATUS_PHRASES[status_code] + return status_code + except Exception: + return StandardResponseCode.HTTP_400 + + +async def _validation_exception_handler(request: Request, exc: RequestValidationError | ValidationError): + errors = [] + for error in exc.errors(): + custom_message = CUSTOM_VALIDATION_ERROR_MESSAGES.get(error['type']) + if custom_message: + ctx = error.get('ctx') + if not ctx: + error['msg'] = custom_message + else: + error['msg'] = custom_message.format(**ctx) + ctx_error = ctx.get('error') + if ctx_error: + error['ctx']['error'] = ( + ctx_error.__str__().replace("'", '"') if isinstance(ctx_error, Exception) else None + ) + errors.append(error) + error = errors[0] + if error.get('type') == 'json_invalid': + message = 'json解析失败' + else: + error_input = error.get('input') + field = str(error.get('loc')[-1]) + error_msg = error.get('msg') + message = f'{field} {error_msg},输入:{error_input}' if settings.ENVIRONMENT == 'dev' else error_msg + msg = f'请求参数非法: {message}' + data = {'errors': errors} if settings.ENVIRONMENT == 'dev' else None + content = { + 'code': StandardResponseCode.HTTP_422, + 'msg': msg, + 'data': data, + } + request.state.__request_validation_exception__ = content + content.update(trace_id=get_request_trace_id(request)) + return MsgSpecJSONResponse(status_code=422, content=content) + + +def register_exception(app: FastAPI): + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + if settings.ENVIRONMENT == 'dev': + content = { + 'code': exc.status_code, + 'msg': exc.detail, + 'data': None, + } + else: + res = response_base.fail(res=CustomResponseCode.HTTP_400) + content = res.model_dump() + request.state.__request_http_exception__ = content + content.update(trace_id=get_request_trace_id(request)) + return MsgSpecJSONResponse( + status_code=_get_exception_code(exc.status_code), + content=content, + headers=exc.headers, + ) + + @app.exception_handler(RequestValidationError) + async def fastapi_validation_exception_handler(request: Request, exc: RequestValidationError): + return await _validation_exception_handler(request, exc) + + @app.exception_handler(ValidationError) + async def pydantic_validation_exception_handler(request: Request, exc: ValidationError): + return await _validation_exception_handler(request, exc) + + @app.exception_handler(AssertionError) + async def assertion_error_handler(request: Request, exc: AssertionError): + if settings.ENVIRONMENT == 'dev': + content = { + 'code': StandardResponseCode.HTTP_500, + 'msg': str(''.join(exc.args) if exc.args else exc.__doc__), + 'data': None, + } + else: + res = response_base.fail(res=CustomResponseCode.HTTP_500) + content = res.model_dump() + request.state.__request_assertion_error__ = content + content.update(trace_id=get_request_trace_id(request)) + return MsgSpecJSONResponse( + status_code=StandardResponseCode.HTTP_500, + content=content, + ) + + @app.exception_handler(BaseExceptionMixin) + async def custom_exception_handler(request: Request, exc: BaseExceptionMixin): + content = { + 'code': exc.code, + 'msg': str(exc.msg), + 'data': exc.data if exc.data else None, + } + request.state.__request_custom_exception__ = content + content.update(trace_id=get_request_trace_id(request)) + return MsgSpecJSONResponse( + status_code=_get_exception_code(exc.code), + content=content, + background=exc.background, + ) + + @app.exception_handler(Exception) + async def all_unknown_exception_handler(request: Request, exc: Exception): + if settings.ENVIRONMENT == 'dev': + content = { + 'code': StandardResponseCode.HTTP_500, + 'msg': str(exc), + 'data': None, + } + else: + res = response_base.fail(res=CustomResponseCode.HTTP_500) + content = res.model_dump() + request.state.__request_all_unknown_exception__ = content + content.update(trace_id=get_request_trace_id(request)) + return MsgSpecJSONResponse( + status_code=StandardResponseCode.HTTP_500, + content=content, + ) + + if settings.MIDDLEWARE_CORS: + + @app.exception_handler(StandardResponseCode.HTTP_500) + async def cors_custom_code_500_exception_handler(request, exc): + """ + 500 + + `Related issue `_ + + `Solution `_ + + :param request: FastAPI + :param exc: + :return: + """ + if isinstance(exc, BaseExceptionMixin): + content = { + 'code': exc.code, + 'msg': exc.msg, + 'data': exc.data, + } + else: + if settings.ENVIRONMENT == 'dev': + content = { + 'code': StandardResponseCode.HTTP_500, + 'msg': str(exc), + 'data': None, + } + else: + res = response_base.fail(res=CustomResponseCode.HTTP_500) + content = res.model_dump() + request.state.__request_cors_500_exception__ = content + content.update(trace_id=get_request_trace_id(request)) + response = MsgSpecJSONResponse( + status_code=exc.code if isinstance(exc, BaseExceptionMixin) else StandardResponseCode.HTTP_500, + content=content, + background=exc.background if isinstance(exc, BaseExceptionMixin) else None, + ) + origin = request.headers.get('origin') + if origin: + cors = CORSMiddleware( + app=app, + allow_origins=settings.CORS_ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], + expose_headers=settings.CORS_EXPOSE_HEADERS, + ) + response.headers.update(cors.simple_headers) + has_cookie = 'cookie' in request.headers + if cors.allow_all_origins and has_cookie: + response.headers['Access-Control-Allow-Origin'] = origin + elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin): + response.headers['Access-Control-Allow-Origin'] = origin + response.headers.add_vary_header('Origin') + return response diff --git a/common/external/__pycache__/external_api.cpython-310.pyc b/common/external/__pycache__/external_api.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71d1fd3ff9f186c007c2d7524c450fac0da32ff4 Binary files /dev/null and b/common/external/__pycache__/external_api.cpython-310.pyc differ diff --git a/common/external/external_api.py b/common/external/external_api.py new file mode 100644 index 0000000000000000000000000000000000000000..3e2091aca3e759d200c80ef43a9c972a0d02b49f --- /dev/null +++ b/common/external/external_api.py @@ -0,0 +1,83 @@ +import httpx +from core.conf import settings +from typing import Optional, Dict, Any +from core.token_manager import get_access_token + +API_BASE_URL = settings.API_BASE_URL + +class API(): + @staticmethod + async def get(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, api_base: str = None): + if api_base: + url = f"{api_base}{endpoint}" + else: + url = f"{API_BASE_URL}{endpoint}" + access_token = get_access_token() + headers = headers or {} + headers.setdefault("Content-Type", "application/json") + headers["Authorization"] = f"Bearer {access_token}" + async with httpx.AsyncClient() as client: + try: + response = await client.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as http_err: + return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} + except Exception as err: + return {"error": f"Request failed: {str(err)}"} + + @staticmethod + async def post(endpoint: str , payload: Dict[str, Any] = None,headers: Optional[Dict[str, str]] = None, api_base: str = None): + if api_base: + url = f"{api_base}{endpoint}" + else: + url = f"{API_BASE_URL}{endpoint}" + access_token = get_access_token() + headers = headers or {} + headers.setdefault("Content-Type", "application/json") + headers["Authorization"] = f"Bearer {access_token}" + async with httpx.AsyncClient() as client: + try: + response = await client.post(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as http_err: + return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} + except Exception as err: + return {"error": f"Request failed: {str(err)}"} + + @staticmethod + async def put_api(endpoint: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None): + url = f"{API_BASE_URL}{endpoint}" + access_token = get_access_token() + headers = headers or {} + headers.setdefault("Content-Type", "application/json") + headers["Authorization"] = f"Bearer {access_token}" + async with httpx.AsyncClient() as client: + try: + response = await client.put(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as http_err: + return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} + except Exception as err: + return {"error": f"Request failed: {str(err)}"} + + @staticmethod + async def delete_api(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None): + url = f"{API_BASE_URL}{endpoint}" + access_token = get_access_token() + headers = headers or {} + headers.setdefault("Content-Type", "application/json") + headers["Authorization"] = f"Bearer {access_token}" + async with httpx.AsyncClient() as client: + try: + response = await client.delete(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as http_err: + return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"} + except Exception as err: + return {"error": f"Request failed: {str(err)}"} + +api: API = API() \ No newline at end of file diff --git a/common/log.py b/common/log.py new file mode 100644 index 0000000000000000000000000000000000000000..32ee83e5ae627db445adafce20a7a5d047a85288 --- /dev/null +++ b/common/log.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import inspect +import logging +import os +import sys + +from asgi_correlation_id import correlation_id +from loguru import logger + +from core import path_conf +from core.conf import settings + + +class InterceptHandler(logging.Handler): + def emit(self, record: logging.LogRecord): + # 获取对应的 Loguru 级别(如果存在) + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # 查找记录日志消息的调用者 + frame, depth = inspect.currentframe(), 0 + while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__): + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + + +def setup_logging() -> None: + """ + - https://github.com/benoitc/gunicorn/issues/1572#issuecomment-638391953 + - https://github.com/pawamoy/pawamoy.github.io/issues/17 + """ + logging.root.handlers = [InterceptHandler()] + logging.root.setLevel(settings.LOG_STD_LEVEL) + + for name in logging.root.manager.loggerDict.keys(): + logging.getLogger(name).handlers = [] + if 'uvicorn.access' in name or 'watchfiles.main' in name: + logging.getLogger(name).propagate = False + else: + logging.getLogger(name).propagate = True + + # Debug log handlers + # logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}') + + # https://github.com/snok/asgi-correlation-id/issues/7 + def correlation_id_filter(record): + cid = correlation_id.get(settings.LOG_CID_DEFAULT_VALUE) + record['correlation_id'] = cid[: settings.LOG_CID_UUID_LENGTH] + return record + + # 配置 loguru 处理器 + logger.remove() # 移除默认处理器 + logger.configure( + handlers=[ + { + 'sink': sys.stdout, + 'level': settings.LOG_STD_LEVEL, + 'filter': lambda record: correlation_id_filter(record), + 'format': settings.LOG_STD_FORMAT, + } + ] + ) + + +def set_custom_logfile() -> None: + log_path = path_conf.LOG_DIR + if not os.path.exists(log_path): + os.mkdir(log_path) + + log_access_file = os.path.join(log_path, settings.LOG_ACCESS_FILENAME) + log_error_file = os.path.join(log_path, settings.LOG_ERROR_FILENAME) + + # https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add + log_config = { + 'format': settings.LOG_FILE_FORMAT, + 'enqueue': True, + 'rotation': '5 MB', + 'retention': '7 days', + 'compression': 'tar.gz', + } + + logger.add( + str(log_access_file), + level=settings.LOG_ACCESS_FILE_LEVEL, + filter=lambda record: record['level'].no <= 25, + backtrace=False, + diagnose=False, + **log_config, + ) + + logger.add( + str(log_error_file), + level=settings.LOG_ERROR_FILE_LEVEL, + filter=lambda record: record['level'].no >= 30, + backtrace=True, + diagnose=True, + **log_config, + ) + + +log = logger diff --git a/common/response/__init__.py b/common/response/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..56fafa58b3f43decb7699b93048b8b87e0f695aa --- /dev/null +++ b/common/response/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/common/response/__pycache__/__init__.cpython-310.pyc b/common/response/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..840f851b862be58eaa9ede32ea5558eebe46583f Binary files /dev/null and b/common/response/__pycache__/__init__.cpython-310.pyc differ diff --git a/common/response/__pycache__/__init__.cpython-39.pyc b/common/response/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee22120755e6c784a8f599f434cf90c016ff8e12 Binary files /dev/null and b/common/response/__pycache__/__init__.cpython-39.pyc differ diff --git a/common/response/__pycache__/respone_dialogflow.cpython-310.pyc b/common/response/__pycache__/respone_dialogflow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f34caa04319031829cbb8f4739d01e521ffe1dbd Binary files /dev/null and b/common/response/__pycache__/respone_dialogflow.cpython-310.pyc differ diff --git a/common/response/__pycache__/response_code.cpython-310.pyc b/common/response/__pycache__/response_code.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45a7e320d7d6d4d95747ea5509e1e567909dbf07 Binary files /dev/null and b/common/response/__pycache__/response_code.cpython-310.pyc differ diff --git a/common/response/__pycache__/response_code.cpython-39.pyc b/common/response/__pycache__/response_code.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be3926186ae3ab673bdf6c207c9e6d63647babaf Binary files /dev/null and b/common/response/__pycache__/response_code.cpython-39.pyc differ diff --git a/common/response/__pycache__/response_schema.cpython-310.pyc b/common/response/__pycache__/response_schema.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4801de30c982392280e7eb625a154eccb56956ec Binary files /dev/null and b/common/response/__pycache__/response_schema.cpython-310.pyc differ diff --git a/app/types/Respone.py b/common/response/respone_dialogflow.py similarity index 92% rename from app/types/Respone.py rename to common/response/respone_dialogflow.py index 4350d7172c48ddb5b80f489bdda0ff021789a2db..11d3b65309420d23669f1242b649b9d6c915c067 100644 --- a/app/types/Respone.py +++ b/common/response/respone_dialogflow.py @@ -7,7 +7,7 @@ class MessageText(BaseModel): class Message(BaseModel): text: Optional[MessageText] = None - payload: Optional[Dict[str, Any]] = None # Thêm trường payload tùy chọn + payload: Optional[Dict[str, Any]] = None diff --git a/common/response/response_code.py b/common/response/response_code.py new file mode 100644 index 0000000000000000000000000000000000000000..1a1b1d0b7451f89093e11a9b5b05fa8823bf7c2c --- /dev/null +++ b/common/response/response_code.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import dataclasses + +from enum import Enum + + +class CustomCodeBase(Enum): + + @property + def code(self) -> int: + return self.value[0] + + @property + def msg(self) -> str: + return self.value[1] + + +class CustomResponseCode(CustomCodeBase): + + HTTP_200 = (200, 'Request successful') + HTTP_201 = (201, 'Resource created') + HTTP_202 = (202, 'Request accepted but not completed yet') + HTTP_204 = (204, 'Request successful but no content returned') + HTTP_400 = (400, 'Bad request') + HTTP_401 = (401, 'Unauthorized') + HTTP_403 = (403, 'Forbidden') + HTTP_404 = (404, 'Resource not found') + HTTP_410 = (410, 'Resource permanently deleted') + HTTP_422 = (422, 'Invalid parameters') + HTTP_425 = (425, 'Too early, server can’t handle it') + HTTP_429 = (429, 'Too many requests') + HTTP_500 = (500, 'Internal server error') + HTTP_502 = (502, 'Bad gateway') + HTTP_503 = (503, 'Service unavailable') + HTTP_504 = (504, 'Gateway timeout') + + +class CustomErrorCode(CustomCodeBase): + + CAPTCHA_ERROR = (40001, 'The verification code is incorrect') + + +@dataclasses.dataclass +class CustomResponse: + """ + Provides an open-style response structure. + Useful if you want to define response code & message freely. + """ + + code: int + msg: str + + +class StandardResponseCode: + + """ + HTTP codes + See HTTP Status Code Registry: + https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + + And RFC 2324 - https://tools.ietf.org/html/rfc2324 + """ + HTTP_100 = 100 # CONTINUE: + HTTP_101 = 101 # SWITCHING_PROTOCOLS: + HTTP_102 = 102 # PROCESSING: + HTTP_103 = 103 # EARLY_HINTS: + HTTP_200 = 200 # OK: + HTTP_201 = 201 # CREATED: + HTTP_202 = 202 # ACCEPTED: + HTTP_203 = 203 # NON_AUTHORITATIVE_INFORMATION: + HTTP_204 = 204 # NO_CONTENT: + HTTP_205 = 205 # RESET_CONTENT: + HTTP_206 = 206 # PARTIAL_CONTENT: + HTTP_207 = 207 # MULTI_STATUS: + HTTP_208 = 208 # ALREADY_REPORTED: + HTTP_226 = 226 # IM_USED: + HTTP_300 = 300 # MULTIPLE_CHOICES: + HTTP_301 = 301 # MOVED_PERMANENTLY: + HTTP_302 = 302 # FOUND: + HTTP_303 = 303 # SEE_OTHER: + HTTP_304 = 304 # NOT_MODIFIED: + HTTP_305 = 305 # USE_PROXY: + HTTP_307 = 307 # TEMPORARY_REDIRECT: + HTTP_308 = 308 # PERMANENT_REDIRECT: + HTTP_400 = 400 # BAD_REQUEST: + HTTP_401 = 401 # UNAUTHORIZED: + HTTP_402 = 402 # PAYMENT_REQUIRED: + HTTP_403 = 403 # FORBIDDEN: + HTTP_404 = 404 # NOT_FOUND: + HTTP_405 = 405 # METHOD_NOT_ALLOWED: + HTTP_406 = 406 # NOT_ACCEPTABLE: + HTTP_407 = 407 # PROXY_AUTHENTICATION_REQUIRED: + HTTP_408 = 408 # REQUEST_TIMEOUT: + HTTP_409 = 409 # CONFLICT: + HTTP_410 = 410 # GONE: + HTTP_411 = 411 # LENGTH_REQUIRED: + HTTP_412 = 412 # PRECONDITION_FAILED: + HTTP_413 = 413 # REQUEST_ENTITY_TOO_LARGE: + HTTP_414 = 414 # REQUEST_URI_TOO_LONG: + HTTP_415 = 415 # UNSUPPORTED_MEDIA_TYPE: + HTTP_416 = 416 # REQUESTED_RANGE_NOT_SATISFIABLE: + HTTP_417 = 417 # EXPECTATION_FAILED: + HTTP_418 = 418 # UNUSED: + HTTP_421 = 421 # MISDIRECTED_REQUEST: + HTTP_422 = 422 # UNPROCESSABLE_CONTENT: + HTTP_423 = 423 # LOCKED: + HTTP_424 = 424 # FAILED_DEPENDENCY: + HTTP_425 = 425 # TOO_EARLY: + HTTP_426 = 426 # UPGRADE_REQUIRED: + HTTP_427 = 427 # UNASSIGNED: + HTTP_428 = 428 # PRECONDITION_REQUIRED: + HTTP_429 = 429 # TOO_MANY_REQUESTS: + HTTP_430 = 430 # Unassigned: + HTTP_431 = 431 # REQUEST_HEADER_FIELDS_TOO_LARGE: + HTTP_451 = 451 # UNAVAILABLE_FOR_LEGAL_REASONS: + HTTP_500 = 500 # INTERNAL_SERVER_ERROR: + HTTP_501 = 501 # NOT_IMPLEMENTED: + HTTP_502 = 502 # BAD_GATEWAY: + HTTP_503 = 503 # SERVICE_UNAVAILABLE: + HTTP_504 = 504 # GATEWAY_TIMEOUT: + HTTP_505 = 505 # HTTP_VERSION_NOT_SUPPORTED: HTTP + HTTP_506 = 506 # VARIANT_ALSO_NEGOTIATES: + HTTP_507 = 507 # INSUFFICIENT_STORAGE: + HTTP_508 = 508 # LOOP_DETECTED: + HTTP_509 = 509 # UNASSIGNED: + HTTP_510 = 510 # NOT_EXTENDED: + HTTP_511 = 511 # NETWORK_AUTHENTICATION_REQUIRED: + + """ + WebSocket codes + https://www.iana.org/assignments/websocket/websocket.xml#close-code-number + https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + """ + WS_1000 = 1000 # NORMAL_CLOSURE: + WS_1001 = 1001 # GOING_AWAY: + WS_1002 = 1002 # PROTOCOL_ERROR: + WS_1003 = 1003 # UNSUPPORTED_DATA: + WS_1005 = 1005 # NO_STATUS_RCVD: + WS_1006 = 1006 # ABNORMAL_CLOSURE: + WS_1007 = 1007 # INVALID_FRAME_PAYLOAD_DATA: + WS_1008 = 1008 # POLICY_VIOLATION: + WS_1009 = 1009 # MESSAGE_TOO_BIG: + WS_1010 = 1010 # MANDATORY_EXT: + WS_1011 = 1011 # INTERNAL_ERROR: + WS_1012 = 1012 # SERVICE_RESTART: + WS_1013 = 1013 # TRY_AGAIN_LATER: + WS_1014 = 1014 # BAD_GATEWAY: + WS_1015 = 1015 # TLS_HANDSHAKE: + WS_3000 = 3000 # UNAUTHORIZED: + WS_3003 = 3003 # FORBIDDEN: diff --git a/common/response/response_schema.py b/common/response/response_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..b767fb22a6475c8476017c231b30776a50f7a09c --- /dev/null +++ b/common/response/response_schema.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Any, Generic, TypeVar + +from fastapi import Response +from pydantic import BaseModel, Field + +from common.response.response_code import CustomResponse, CustomResponseCode +from utils.serializers import MsgSpecJSONResponse + +SchemaT = TypeVar('SchemaT') + + +class ResponseModel(BaseModel): + """ + + @router.get('/test', response_model=ResponseModel) + def test(): + return ResponseModel(data={'test': 'test'}) + + + @router.get('/test') + def test() -> ResponseModel: + return ResponseModel(data={'test': 'test'}) + + + @router.get('/test') + def test() -> ResponseModel: + res = CustomResponseCode.HTTP_200 + return ResponseModel(code=res.code, msg=res.msg, data={'test': 'test'}) + """ + + code: int = Field(CustomResponseCode.HTTP_200.code, description='Return status code') + msg: str = Field(CustomResponseCode.HTTP_200.msg, description='Return message') + data: Any | None = Field(None, description='Return data') + + +class ResponseSchemaModel(ResponseModel, Generic[SchemaT]): + """ + + @router.get('/test', response_model=ResponseSchemaModel[GetApiDetail]) + def test(): + return ResponseSchemaModel[GetApiDetail](data=GetApiDetail(...)) + + + @router.get('/test') + def test() -> ResponseSchemaModel[GetApiDetail]: + return ResponseSchemaModel[GetApiDetail](data=GetApiDetail(...)) + + + @router.get('/test') + def test() -> ResponseSchemaModel[GetApiDetail]: + res = CustomResponseCode.HTTP_200 + return ResponseSchemaModel[GetApiDetail](code=res.code, msg=res.msg, data=GetApiDetail(...)) + """ + + data: SchemaT + + +class ResponseBase: + """""" + + @staticmethod + def __response( + *, res: CustomResponseCode | CustomResponse = None, data: Any | None = None + ) -> ResponseModel | ResponseSchemaModel: + """" + :param res: + :param data: + :return: + """ + return ResponseModel(code=res.code, msg=res.msg, data=data) + + def success( + self, + *, + res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200, + data: Any | None = None, + ) -> ResponseModel | ResponseSchemaModel: + """ + + + :param res: + :param data: + :return: + """ + return self.__response(res=res, data=data) + + def fail( + self, + *, + res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_400, + data: Any = None, + ) -> ResponseModel | ResponseSchemaModel: + """ + + :param res: + :param data: + :return: + """ + return self.__response(res=res, data=data) + + @staticmethod + def fast_success( + *, + res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200, + data: Any | None = None, + ) -> Response: + return MsgSpecJSONResponse({'code': res.code, 'msg': res.msg, 'data': data}) + + +response_base: ResponseBase = ResponseBase() diff --git a/common/schema.py b/common/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..ed458aeef593d813a6b163a1f8cbce31c88d7570 --- /dev/null +++ b/common/schema.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Annotated + +from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email + +from core.conf import settings + +# 自定义验证错误信息,参考: +# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 +# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 +CUSTOM_VALIDATION_ERROR_MESSAGES = { + 'no_such_attribute': "对象没有属性 '{attribute}'", + 'json_invalid': '无效的 JSON: {error}', + 'json_type': 'JSON 输入应为字符串、字节或字节数组', + 'recursion_loop': '递归错误 - 检测到循环引用', + 'model_type': '输入应为有效的字典或 {class_name} 的实例', + 'model_attributes_type': '输入应为有效的字典或可提取字段的对象', + 'dataclass_exact_type': '输入应为 {class_name} 的实例', + 'dataclass_type': '输入应为字典或 {class_name} 的实例', + 'missing': '字段为必填项', + 'frozen_field': '字段已冻结', + 'frozen_instance': '实例已冻结', + 'extra_forbidden': '不允许额外的输入', + 'invalid_key': '键应为字符串', + 'get_attribute_error': '提取属性时出错: {error}', + 'none_required': '输入应为 None', + 'enum': '输入应为 {expected}', + 'greater_than': '输入应大于 {gt}', + 'greater_than_equal': '输入应大于或等于 {ge}', + 'less_than': '输入应小于 {lt}', + 'less_than_equal': '输入应小于或等于 {le}', + 'finite_number': '输入应为有限数字', + 'too_short': '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}', + 'too_long': '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}', + 'string_type': '输入应为有效的字符串', + 'string_sub_type': '输入应为字符串,而不是 str 子类的实例', + 'string_unicode': '输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串', + 'string_pattern_mismatch': "字符串应匹配模式 '{pattern}'", + 'string_too_short': '字符串应至少有 {min_length} 个字符', + 'string_too_long': '字符串最多应有 {max_length} 个字符', + 'dict_type': '输入应为有效的字典', + 'mapping_type': '输入应为有效的映射,错误: {error}', + 'iterable_type': '输入应为可迭代对象', + 'iteration_error': '迭代对象时出错,错误: {error}', + 'list_type': '输入应为有效的列表', + 'tuple_type': '输入应为有效的元组', + 'set_type': '输入应为有效的集合', + 'bool_type': '输入应为有效的布尔值', + 'bool_parsing': '输入应为有效的布尔值,无法解释输入', + 'int_type': '输入应为有效的整数', + 'int_parsing': '输入应为有效的整数,无法将字符串解析为整数', + 'int_parsing_size': '无法将输入字符串解析为整数,超出最大大小', + 'int_from_float': '输入应为有效的整数,得到一个带有小数部分的数字', + 'multiple_of': '输入应为 {multiple_of} 的倍数', + 'float_type': '输入应为有效的数字', + 'float_parsing': '输入应为有效的数字,无法将字符串解析为数字', + 'bytes_type': '输入应为有效的字节', + 'bytes_too_short': '数据应至少有 {min_length} 个字节', + 'bytes_too_long': '数据最多应有 {max_length} 个字节', + 'value_error': '值错误,{error}', + 'assertion_error': '断言失败,{error}', + 'literal_error': '输入应为 {expected}', + 'date_type': '输入应为有效的日期', + 'date_parsing': '输入应为 YYYY-MM-DD 格式的有效日期,{error}', + 'date_from_datetime_parsing': '输入应为有效的日期或日期时间,{error}', + 'date_from_datetime_inexact': '提供给日期的日期时间应具有零时间 - 例如为精确日期', + 'date_past': '日期应为过去的时间', + 'date_future': '日期应为未来的时间', + 'time_type': '输入应为有效的时间', + 'time_parsing': '输入应为有效的时间格式,{error}', + 'datetime_type': '输入应为有效的日期时间', + 'datetime_parsing': '输入应为有效的日期时间,{error}', + 'datetime_object_invalid': '无效的日期时间对象,得到 {error}', + 'datetime_past': '输入应为过去的时间', + 'datetime_future': '输入应为未来的时间', + 'timezone_naive': '输入不应包含时区信息', + 'timezone_aware': '输入应包含时区信息', + 'timezone_offset': '需要时区偏移为 {tz_expected},实际得到 {tz_actual}', + 'time_delta_type': '输入应为有效的时间差', + 'time_delta_parsing': '输入应为有效的时间差,{error}', + 'frozen_set_type': '输入应为有效的冻结集合', + 'is_instance_of': '输入应为 {class} 的实例', + 'is_subclass_of': '输入应为 {class} 的子类', + 'callable_type': '输入应为可调用对象', + 'union_tag_invalid': "使用 {discriminator} 找到的输入标签 '{tag}' 与任何预期标签不匹配: {expected_tags}", + 'union_tag_not_found': '无法使用区分器 {discriminator} 提取标签', + 'arguments_type': '参数必须是元组、列表或字典', + 'missing_argument': '缺少必需参数', + 'unexpected_keyword_argument': '意外的关键字参数', + 'missing_keyword_only_argument': '缺少必需的关键字专用参数', + 'unexpected_positional_argument': '意外的位置参数', + 'missing_positional_only_argument': '缺少必需的位置专用参数', + 'multiple_argument_values': '为参数提供了多个值', + 'url_type': 'URL 输入应为字符串或 URL', + 'url_parsing': '输入应为有效的 URL,{error}', + 'url_syntax_violation': '输入违反了严格的 URL 语法规则,{error}', + 'url_too_long': 'URL 最多应有 {max_length} 个字符', + 'url_scheme': 'URL 方案应为 {expected_schemes}', + 'uuid_type': 'UUID 输入应为字符串、字节或 UUID 对象', + 'uuid_parsing': '输入应为有效的 UUID,{error}', + 'uuid_version': '预期 UUID 版本为 {expected_version}', + 'decimal_type': '十进制输入应为整数、浮点数、字符串或 Decimal 对象', + 'decimal_parsing': '输入应为有效的十进制数', + 'decimal_max_digits': '十进制输入总共应不超过 {max_digits} 位数字', + 'decimal_max_places': '十进制输入应不超过 {decimal_places} 位小数', + 'decimal_whole_digits': '十进制输入在小数点前应不超过 {whole_digits} 位数字', +} + +CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')] + + +class CustomEmailStr(EmailStr): + """自定义邮箱类型""" + + @classmethod + def _validate(cls, __input_value: str) -> str: + return None if __input_value == '' else validate_email(__input_value)[1] + + +class SchemaBase(BaseModel): + """基础模型配置""" + + model_config = ConfigDict( + use_enum_values=True, + json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)}, + ) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/core/__pycache__/__init__.cpython-310.pyc b/core/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bf9677615fa3197e66e67d1a5b24666ca702903 Binary files /dev/null and b/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/core/__pycache__/__init__.cpython-39.pyc b/core/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a16a5b21810034d0b6f0fcf6269bc760a132382d Binary files /dev/null and b/core/__pycache__/__init__.cpython-39.pyc differ diff --git a/core/__pycache__/conf.cpython-310.pyc b/core/__pycache__/conf.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edd638c76c54bb72cef0a9fec39d37f86e8cfa04 Binary files /dev/null and b/core/__pycache__/conf.cpython-310.pyc differ diff --git a/app/core/__pycache__/config.cpython-39.pyc b/core/__pycache__/config.cpython-39.pyc similarity index 100% rename from app/core/__pycache__/config.cpython-39.pyc rename to core/__pycache__/config.cpython-39.pyc diff --git a/core/__pycache__/path_conf.cpython-310.pyc b/core/__pycache__/path_conf.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2994329fc476ac84fbda73095f8a27d5f5c7b73 Binary files /dev/null and b/core/__pycache__/path_conf.cpython-310.pyc differ diff --git a/core/__pycache__/registrar.cpython-310.pyc b/core/__pycache__/registrar.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82b6aebb754e72d9d45ed8a2ad98675571bbab57 Binary files /dev/null and b/core/__pycache__/registrar.cpython-310.pyc differ diff --git a/core/__pycache__/registrar.cpython-39.pyc b/core/__pycache__/registrar.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7089971fed1e090a60736b95edde089f68f5377a Binary files /dev/null and b/core/__pycache__/registrar.cpython-39.pyc differ diff --git a/core/__pycache__/token_manager.cpython-310.pyc b/core/__pycache__/token_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37b52a181fa70da3482899c525682ab1339a0cd9 Binary files /dev/null and b/core/__pycache__/token_manager.cpython-310.pyc differ diff --git a/core/conf.py b/core/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..d50b077a4661918811b0657e8afed063e4258523 --- /dev/null +++ b/core/conf.py @@ -0,0 +1,113 @@ +import os +from dotenv import load_dotenv +from functools import lru_cache +from typing import Any, Literal +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + +from core.path_conf import BASE_PATH + +load_dotenv() + +class Settings(BaseSettings): + + model_config = SettingsConfigDict( + env_file = f'{BASE_PATH}/.env', + env_file_encoding = 'utf-8', + extra="ignore", + case_sensitive=True + ) + + ENVIRONMENT: Literal['dev', 'pro'] + + + API_BASE_URL: str + API_ACCESS_TOKEN: str + + # DATABASE_TYPE: Literal['mysql', 'postgresql'] + # DATABASE_HOST: str + # DATABASE_PORT: int + # DATABASE_USER: str + # DATABASE_PASSWORD: str + + # FastAPI + FASTAPI_API_V1_PATH: str = '/api/v1' + FASTAPI_TITLE: str = 'FastAPI' + FASTAPI_VERSION: str = '0.0.1' + FASTAPI_DESCRIPTION: str = 'FastAPI' + FASTAPI_DOCS_URL: str = '/docs' + FASTAPI_REDOC_URL: str = '/redoc' + FASTAPI_OPENAPI_URL: str | None = '/openapi' + FASTAPI_STATIC_FILES: bool = True + + + DATABASE_ECHO: bool = False + DATABASE_POOL_ECHO: bool = False + DATABASE_SCHEMA: str = 'fba' + DATABASE_CHARSET: str = 'utf8mb4' + + + CORS_ALLOWED_ORIGINS: list[str] = [ + 'http://127.0.0.1:8000', + 'http://localhost:5173', + ] + CORS_EXPOSE_HEADERS: list[str] = [ + 'X-Request-ID', + ] + + MIDDLEWARE_CORS: bool = True + MIDDLEWARE_ACCESS: bool = True + + DATETIME_TIMEZONE: str = 'Asia/Ho_Chi_Minh' + DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' + + UPLOAD_READ_SIZE: int = 1024 + UPLOAD_IMAGE_EXT_INCLUDE: list[str] = ['jpg', 'jpeg', 'png', 'gif', 'webp'] + UPLOAD_IMAGE_SIZE_MAX: int = 5 * 1024 * 1024 # 5 MB + UPLOAD_VIDEO_EXT_INCLUDE: list[str] = ['mp4', 'mov', 'avi', 'flv'] + UPLOAD_VIDEO_SIZE_MAX: int = 20 * 1024 * 1024 # 20 MB + + + TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID' + + + # 日志 + LOG_CID_DEFAULT_VALUE: str = '-' + LOG_CID_UUID_LENGTH: int = 32 # 日志 correlation_id 长度,必须小于等于 32 + LOG_STD_LEVEL: str = 'INFO' + LOG_ACCESS_FILE_LEVEL: str = 'INFO' + LOG_ERROR_FILE_LEVEL: str = 'ERROR' + LOG_STD_FORMAT: str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' + ' {correlation_id} | {message}' + ) + LOG_FILE_FORMAT: str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' + ' {correlation_id} | {message}' + ) + LOG_ACCESS_FILENAME: str = 'fba_access.log' + LOG_ERROR_FILENAME: str = 'fba_error.log' + + OPERA_LOG_PATH_EXCLUDE: list[str] = [ + '/favicon.ico', + '/docs', + '/redoc', + '/openapi', + ] + + + @model_validator(mode='before') + @classmethod + def check_env(cls, values: Any) -> Any: + """生产环境下禁用 OpenAPI 文档和静态文件服务""" + if values.get('ENVIRONMENT') == 'pro': + values['FASTAPI_OPENAPI_URL'] = None + values['FASTAPI_STATIC_FILES'] = False + return values + + +@lru_cache +def get_settings() -> Settings: + return Settings() + +settings = get_settings() diff --git a/core/path_conf.py b/core/path_conf.py new file mode 100644 index 0000000000000000000000000000000000000000..d3bd79ebc78edea7830ca85da7907616ae8c64fd --- /dev/null +++ b/core/path_conf.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from pathlib import Path + +BASE_PATH = Path(__file__).resolve().parent.parent + +# ALEMBIC_VERSION_DIR = BASE_PATH / 'alembic' / 'versions' + +LOG_DIR = BASE_PATH / 'log' + +STATIC_DIR = BASE_PATH / 'static' + +UPLOAD_DIR = STATIC_DIR / 'upload' + +JINJA2_TEMPLATE_DIR = BASE_PATH / 'templates' + +# PLUGIN_DIR = BASE_PATH / 'plugin' + +# IP2REGION_XDB = STATIC_DIR / 'ip2region.xdb' diff --git a/core/registrar.py b/core/registrar.py new file mode 100644 index 0000000000000000000000000000000000000000..2f26171858ff0211fe186b9aaf9f123d46d610dc --- /dev/null +++ b/core/registrar.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os + +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from asgi_correlation_id import CorrelationIdMiddleware +from fastapi import Depends, FastAPI +from fastapi_limiter import FastAPILimiter +from fastapi_pagination import add_pagination +from starlette.middleware.authentication import AuthenticationMiddleware +from starlette.staticfiles import StaticFiles + +from common.exception.exception_handler import register_exception +from common.log import set_custom_logfile, setup_logging +from core.conf import settings +from core.path_conf import STATIC_DIR, UPLOAD_DIR +from utils.health_check import ensure_unique_route_names, http_limit_callback +from utils.openapi import simplify_operation_ids +from utils.serializers import MsgSpecJSONResponse + + +# @asynccontextmanager +# async def register_init(app: FastAPI) -> AsyncGenerator[None, None]: +# """ + +# :param app: FastAPI +# :return: +# """ +# await create_table() +# # 连接 redis +# await redis_client.open() +# await FastAPILimiter.init( +# redis=redis_client, +# prefix=settings.REQUEST_LIMITER_REDIS_PREFIX, +# http_callback=http_limit_callback, +# ) + +# yield + +# await redis_client.close() +# await FastAPILimiter.close() + + +def register_app() -> FastAPI: + """ FastAPI """ + app = FastAPI( + title=settings.FASTAPI_TITLE, + version=settings.FASTAPI_VERSION, + description=settings.FASTAPI_DESCRIPTION, + docs_url=settings.FASTAPI_DOCS_URL, + redoc_url=settings.FASTAPI_REDOC_URL, + openapi_url=settings.FASTAPI_OPENAPI_URL, + default_response_class=MsgSpecJSONResponse, + ) + + register_logger() + register_static_file(app) + register_middleware(app) + register_router(app) + register_page(app) + register_exception(app) + + return app + + +def register_logger() -> None: + setup_logging() + set_custom_logfile() + + +def register_static_file(app: FastAPI) -> None: + """ + + :param app: FastAPI + :return: + """ + if not os.path.exists(UPLOAD_DIR): + os.makedirs(UPLOAD_DIR) + app.mount('/static/upload', StaticFiles(directory=UPLOAD_DIR), name='upload') + + if settings.FASTAPI_STATIC_FILES: + app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') + + +def register_middleware(app: FastAPI) -> None: + """ + :param app: FastAPI + :return: + """ + # Opera log + # app.add_middleware(OperaLogMiddleware) + + + + # # Access log + # if settings.MIDDLEWARE_ACCESS: + # from middleware.access_middleware import AccessMiddleware + + # app.add_middleware(AccessMiddleware) + + # Trace ID + app.add_middleware(CorrelationIdMiddleware, validator=False) + + # CORS + if settings.MIDDLEWARE_CORS: + from fastapi.middleware.cors import CORSMiddleware + + app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], + expose_headers=settings.CORS_EXPOSE_HEADERS, + ) + + +def register_router(app: FastAPI) -> None: + """ + + :param app: FastAPI + :return: + """ + from app.router import router + + app.include_router(router) + + ensure_unique_route_names(app) + simplify_operation_ids(app) + + +def register_page(app: FastAPI) -> None: + """ + + :param app: FastAPI + :return: + """ + add_pagination(app) + diff --git a/core/security.py b/core/security.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/core/token_manager.py b/core/token_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..a4a239ec0c5f0a8396bf250a2e17333557e980f1 --- /dev/null +++ b/core/token_manager.py @@ -0,0 +1,28 @@ +import os +import time +import httpx +from core.conf import settings + +_token_cache = { + "access_token": None, + "expires_at": 0, +} +API_ACCESS_TOKEN = settings.API_ACCESS_TOKEN + +async def get_access_token() -> str: + current_time = int(time.time()) + if _token_cache["access_token"] is None or current_time >= _token_cache["expires_at"]: + await refresh_access_token() + return _token_cache["access_token"] + +async def refresh_access_token(): + async with httpx.AsyncClient() as client: + response = await client.post(url=API_ACCESS_TOKEN) + response.raise_for_status() + data = response.json() + access_token = data.get("data") + expires_in = 3600 + + _token_cache["access_token"] = access_token + _token_cache["expires_at"] = int(time.time()) + expires_in - 60 + diff --git a/log/fba_access.log b/log/fba_access.log new file mode 100644 index 0000000000000000000000000000000000000000..3e7ccaaedabedf2e8161bcdca9965eff85426218 --- /dev/null +++ b/log/fba_access.log @@ -0,0 +1,16 @@ +2025-04-08 16:21:07.862 | INFO | - | Started server process [11792] +2025-04-08 16:21:07.863 | INFO | - | Waiting for application startup. +2025-04-08 16:21:07.864 | INFO | - | Application startup complete. +2025-04-08 16:21:07.865 | INFO | - | Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +2025-04-08 16:24:32.721 | INFO | - | Shutting down +2025-04-08 16:24:32.831 | INFO | - | Waiting for application shutdown. +2025-04-08 16:24:32.833 | INFO | - | Application shutdown complete. +2025-04-08 16:24:32.834 | INFO | - | Finished server process [11792] +2025-04-08 16:24:49.453 | INFO | - | Started server process [28056] +2025-04-08 16:24:49.454 | INFO | - | Waiting for application startup. +2025-04-08 16:24:49.455 | INFO | - | Application startup complete. +2025-04-08 16:24:49.456 | INFO | - | Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +2025-04-08 16:26:27.923 | INFO | - | Shutting down +2025-04-08 16:26:28.034 | INFO | - | Waiting for application shutdown. +2025-04-08 16:26:28.036 | INFO | - | Application shutdown complete. +2025-04-08 16:26:28.037 | INFO | - | Finished server process [28056] diff --git a/log/fba_error.log b/log/fba_error.log new file mode 100644 index 0000000000000000000000000000000000000000..2e13aed33bf8c1709ea52c6a6a44db004803523a --- /dev/null +++ b/log/fba_error.log @@ -0,0 +1,227 @@ +2025-04-08 16:21:51.983 | ERROR | 18ac3890a6ca4f82b4f3e2819816eec1 | Exception in ASGI application + +Traceback (most recent call last): + + File "C:\University-HCMUTE\InternFuta\ApiDialogflowChatbot\run.py", line 9, in + server.run() + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\uvicorn\server.py", line 66, in run + return asyncio.run(self.serve(sockets=sockets)) + │ │ │ │ └ None + │ │ │ └ + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\asyncio\runners.py", line 44, in run + return loop.run_until_complete(main) + │ │ └ + │ └ + └ <_WindowsSelectorEventLoop running=True closed=False debug=False> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\asyncio\base_events.py", line 633, in run_until_complete + self.run_forever() + │ └ + └ <_WindowsSelectorEventLoop running=True closed=False debug=False> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\asyncio\base_events.py", line 600, in run_forever + self._run_once() + │ └ + └ <_WindowsSelectorEventLoop running=True closed=False debug=False> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\asyncio\base_events.py", line 1896, in _run_once + handle._run() + │ └ + └ ()> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\asyncio\events.py", line 80, in _run + self._context.run(self._callback, *self._args) + │ │ │ │ │ └ + │ │ │ │ └ ()> + │ │ │ └ + │ │ └ ()> + │ └ + └ ()> + +> File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 403, in run_asgi + result = await app( # type: ignore[func-returns-value] + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__ + return await self.app(scope, receive, send) + │ │ │ │ └ > + │ │ │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\fastapi\applications.py", line 1054, in __call__ + await super().__call__(scope, receive, send) + │ │ └ > + │ └ > + │ │ │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\middleware\errors.py", line 187, in __call__ + raise exc + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\middleware\errors.py", line 165, in __call__ + await self.app(scope, receive, _send) + │ │ │ │ └ ._send at 0x00000245601671C0> + │ │ │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\middleware\cors.py", line 85, in __call__ + await self.app(scope, receive, send) + │ │ │ │ └ ._send at 0x00000245601671C0> + │ │ │ └ , header_name='... + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\asgi_correlation_id\middleware.py", line 90, in __call__ + await self.app(scope, receive, handle_outgoing_request) + │ │ │ │ └ .handle_outgoing_request at 0x0000024560167130> + │ │ │ └ + └ CorrelationIdMiddleware(app=, header_name='... + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + │ │ │ │ │ │ └ .handle_outgoing_request at 0x0000024560167130> + │ │ │ │ │ └ + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + │ │ │ └ .wrapped_app..sender at 0x0000024560167250> + │ │ └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\routing.py", line 714, in __call__ + await self.middleware_stack(scope, receive, send) + │ │ │ │ └ .wrapped_app..sender at 0x0000024560167250> + │ │ │ └ > + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\routing.py", line 734, in app + await route.handle(scope, receive, send) + │ │ │ │ └ .wrapped_app..sender at 0x0000024560167250> + │ │ │ └ + └ APIRoute(path='/api/v1/chatbot', name='index', methods=['GET']) + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\routing.py", line 288, in handle + await self.app(scope, receive, send) + │ │ │ │ └ .wrapped_app..sender at 0x0000024560167250> + │ │ │ └ .app at 0x00000245601669E0> + └ APIRoute(path='/api/v1/chatbot', name='index', methods=['GET']) + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\routing.py", line 76, in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + │ │ │ │ │ └ .wrapped_app..sender at 0x0000024560167250> + │ │ │ │ └ + │ └ .app..app at 0x00000245601672E0> + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + │ │ │ └ .wrapped_app..sender at 0x0000024560167370> + │ │ └ .app..app at 0x00000245601672E0> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\routing.py", line 73, in app + response = await f(request) + │ └ + └ .app at 0x0000024560166950> + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\fastapi\routing.py", line 301, in app + raw_response = await run_endpoint_function( + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\fastapi\routing.py", line 212, in run_endpoint_function + return await dependant.call(**values) + │ │ └ {'request': } + │ └ + └ Dependant(path_params=[], query_params=[], header_params=[], cookie_params=[], body_params=[], dependencies=[], security_requ... + + File "C:\University-HCMUTE\InternFuta\ApiDialogflowChatbot\app\dialogflow\api\v1\dialogflow.py", line 394, in index + return templates.TemplateResponse("index.html", {"request": request, "title": "Dawi Chatbot"}) + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\templating.py", line 208, in TemplateResponse + template = self.get_template(name) + │ │ └ 'index.html' + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\starlette\templating.py", line 131, in get_template + return self.env.get_template(name) + │ │ │ └ 'index.html' + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\jinja2\environment.py", line 1016, in get_template + return self._load_template(name, globals) + │ │ │ └ None + │ │ └ 'index.html' + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\jinja2\environment.py", line 975, in _load_template + template = self.loader.load(self, name, self.make_globals(globals)) + │ │ │ │ │ │ │ └ None + │ │ │ │ │ │ └ + │ │ │ │ │ └ + │ │ │ │ └ 'index.html' + │ │ │ └ + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\jinja2\loaders.py", line 126, in load + source, filename, uptodate = self.get_source(environment, name) + │ │ │ └ 'index.html' + │ │ └ + │ └ + └ + + File "C:\Users\tuf\miniconda3\envs\chatbot-deploy\lib\site-packages\jinja2\loaders.py", line 209, in get_source + raise TemplateNotFound( + └ + +jinja2.exceptions.TemplateNotFound: 'index.html' not found in search path: 'app/templates' diff --git a/main.py b/main.py index f749d3c4e3f41de5d619bc930d79d918f42a7d57..6ea7023f1e05341e2229da54053f36bd392cd9ee 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,3 @@ -from fastapi import FastAPI -from app.api.routes import router +from core.registrar import register_app -app = FastAPI(title="FastAPI Proxy Service") - -app.include_router(router, prefix="/api") - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) +app = register_app() diff --git a/requirements.txt b/requirements.txt index c76f55b4828b20a2b08c95db711228d6ce48efb6..cbd948294a2807eeb91c1ff77ff6c1fa4730660d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,52 @@ annotated-types==0.7.0 anyio==4.8.0 +asgi-correlation-id==4.3.4 +async-timeout==5.0.1 +cachetools==5.5.2 certifi==2025.1.31 +charset-normalizer==3.4.1 click==8.1.8 colorama==0.4.6 exceptiongroup==1.2.2 fastapi==0.115.11 +fastapi-limiter==0.1.6 +fastapi-pagination==0.12.34 +google-api-core==2.24.1 +google-auth==2.38.0 +google-cloud-dialogflow-cx==1.40.0 +googleapis-common-protos==1.69.1 +grpcio==1.71.0rc2 +grpcio-status==1.71.0rc2 h11==0.14.0 httpcore==1.0.7 httptools==0.6.4 httpx==0.28.1 idna==3.10 Jinja2==3.1.6 +loguru==0.7.3 MarkupSafe==3.0.2 -pydantic==2.10.6 -pydantic_core==2.27.2 +msgspec==0.19.0 +opencv-contrib-python==4.9.0.80 +packaging==24.2 +proto-plus==1.26.0 +protobuf==5.29.3 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +pydantic==2.11.2 +pydantic-settings==2.8.1 +pydantic_core==2.33.1 python-dotenv==1.0.1 PyYAML==6.0.2 +redis==6.0.0b2 +requests==2.32.3 +rsa==4.9 sniffio==1.3.1 +sounddevice==0.4.6 starlette==0.46.0 +typing-inspection==0.4.0 typing_extensions==4.12.2 +urllib3==2.3.0 uvicorn==0.34.0 watchfiles==1.0.4 websockets==15.0.1 +win32_setctime==1.2.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..cd767ecdf43d5aa716d1fe0318d3e2dcd82ff808 --- /dev/null +++ b/run.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import uvicorn + +if __name__ == '__main__': + try: + config = uvicorn.Config(app='main:app', reload=True) + server = uvicorn.Server(config) + server.run() + except Exception as e: + raise e diff --git a/app/utils/code_province.json b/static/files/code_province.json similarity index 100% rename from app/utils/code_province.json rename to static/files/code_province.json diff --git a/static/files/origin-codes.json b/static/files/origin-codes.json new file mode 100644 index 0000000000000000000000000000000000000000..29f6903d8428dd3d1f97f0022a64273a1088a835 --- /dev/null +++ b/static/files/origin-codes.json @@ -0,0 +1,875 @@ +[ + { + "parentId": 11925, + "type": "1", + "name": "An Giang", + "tags": "An Giang", + "level": 2, + "code": "ANGIANG", + "id": 1 + }, + { + "parentId": 11927, + "type": "1", + "name": "Bà Rịa - Vũng Tàu", + "tags": "Ba Ria - Vung Tau", + "level": 2, + "code": "VUNGTAU", + "id": 2 + }, + { + "parentId": 11925, + "type": "1", + "name": "Bạc Liêu", + "tags": "Bac Lieu", + "level": 2, + "code": "BACLIEU", + "id": 3 + }, + { + "parentId": 11923, + "type": "1", + "name": "Bắc Kạn", + "tags": "Bac Kan", + "level": 2, + "code": "BACKAN", + "id": 4 + }, + { + "parentId": 11923, + "type": "1", + "name": "Bắc Giang", + "tags": "Bac Giang", + "level": 2, + "code": "BACGIANG", + "id": 5 + }, + { + "parentId": 11923, + "type": "1", + "name": "Bắc Ninh", + "tags": "Bac Ninh", + "level": 2, + "code": "BACNINH", + "id": 6 + }, + { + "parentId": 11925, + "type": "1", + "name": "Bến Tre", + "tags": "Ben Tre", + "level": 2, + "code": "BENTRE", + "id": 7 + }, + { + "parentId": 11927, + "type": "1", + "name": "Bình Dương", + "tags": "Binh Duong", + "level": 2, + "code": "BINHDUONG", + "id": 8 + }, + { + "parentId": 11924, + "type": "1", + "name": "Bình Định", + "tags": "Binh Dinh", + "level": 2, + "code": "BINHDINH", + "id": 9 + }, + { + "parentId": 11927, + "type": "1", + "name": "Bình Phước", + "tags": "Binh Phuoc", + "level": 2, + "code": "BINHPHUOC", + "id": 10 + }, + { + "parentId": 11924, + "type": "1", + "name": "Bình Thuận", + "tags": "Binh Thuan", + "level": 2, + "code": "BINHTHUAN", + "id": 11 + }, + { + "parentId": 11925, + "type": "1", + "name": "Cà Mau", + "tags": "Ca Mau", + "level": 2, + "code": "CAMAU", + "id": 12 + }, + { + "parentId": 11923, + "type": "1", + "name": "Cao Bằng", + "tags": "Cao Bang", + "level": 2, + "code": "CAOBANG", + "id": 13 + }, + { + "parentId": 11925, + "type": "1", + "name": "Cần Thơ", + "tags": "Can Tho", + "level": 2, + "code": "CANTHO", + "id": 14 + }, + { + "parentId": 11924, + "type": "1", + "name": "Đà Nẵng", + "tags": "Da Nang", + "level": 2, + "code": "DANANG", + "id": 15 + }, + { + "parentId": 11926, + "type": "1", + "name": "Đắk Lắk", + "tags": "Dak Lak", + "level": 2, + "code": "DAKLAK", + "id": 16 + }, + { + "parentId": 11926, + "type": "1", + "name": "Đắk Nông", + "tags": "Dak Nong", + "level": 2, + "code": "DAKNONG", + "id": 17 + }, + { + "parentId": 11927, + "type": "1", + "name": "Đồng Nai", + "tags": "Dong Nai", + "level": 2, + "code": "DONGNAI", + "id": 18 + }, + { + "parentId": 11925, + "type": "1", + "name": "Đồng Tháp", + "tags": "Dong Thap", + "level": 2, + "code": "DONGTHAP", + "id": 19 + }, + { + "parentId": 11923, + "type": "1", + "name": "Điện Biên", + "tags": "Dien Bien", + "level": 2, + "code": "DIENBIEN", + "id": 20 + }, + { + "parentId": 11926, + "type": "1", + "name": "Gia Lai", + "tags": "Gia Lai", + "level": 2, + "code": "GIALAI", + "id": 21 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hà Giang", + "tags": "Ha Giang", + "level": 2, + "code": "HAGIANG", + "id": 22 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hà Nam", + "tags": "Ha Nam", + "level": 2, + "code": "HANAM", + "id": 23 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hà Nội", + "tags": "Ha Noi", + "level": 2, + "code": "HANOI", + "id": 24 + }, + { + "parentId": 11924, + "type": "1", + "name": "Hà Tĩnh", + "tags": "Ha Tinh", + "level": 2, + "code": "HATINH", + "id": 25 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hải Dương", + "tags": "Hai Duong", + "level": 2, + "code": "HAIDUONG", + "id": 26 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hải Phòng", + "tags": "Hai Phong", + "level": 2, + "code": "HAIPHONG", + "id": 27 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hòa Bình", + "tags": "Hoa Binh", + "level": 2, + "code": "HOABINH", + "id": 28 + }, + { + "parentId": 11925, + "type": "1", + "name": "Hậu Giang", + "tags": "Hau Giang", + "level": 2, + "code": "HAUGIANG", + "id": 29 + }, + { + "parentId": 11923, + "type": "1", + "name": "Hưng Yên", + "tags": "Hung Yen", + "level": 2, + "code": "HUNGYEN", + "id": 30 + }, + { + "parentId": 11927, + "type": "1", + "name": "TP. Hồ Chí Minh", + "tags": "TP. Ho Chi Minh", + "level": 2, + "code": "TPHCM", + "id": 31 + }, + { + "parentId": 11924, + "type": "1", + "name": "Khánh Hòa", + "tags": "Khanh Hoa", + "level": 2, + "code": "KHANHHOA", + "id": 32 + }, + { + "parentId": 11925, + "type": "1", + "name": "Kiên Giang", + "tags": "Kien Giang", + "level": 2, + "code": "KIENGIANG", + "id": 33 + }, + { + "parentId": 11926, + "type": "1", + "name": "Kon Tum", + "tags": "Kon Tum", + "level": 2, + "code": "KONTUM", + "id": 34 + }, + { + "parentId": 11923, + "type": "1", + "name": "Lai Châu", + "tags": "Lai Chau", + "level": 2, + "code": "LAICHAU", + "id": 35 + }, + { + "parentId": 11923, + "type": "1", + "name": "Lào Cai", + "tags": "Lao Cai", + "level": 2, + "code": "LAOCAI", + "id": 36 + }, + { + "parentId": 11923, + "type": "1", + "name": "Lạng Sơn", + "tags": "Lang Son", + "level": 2, + "code": "LANGSON", + "id": 37 + }, + { + "parentId": 11926, + "type": "1", + "name": "Lâm Đồng", + "tags": "Lam Dong", + "level": 2, + "code": "LAMDONG", + "id": 38 + }, + { + "parentId": 11925, + "type": "1", + "name": "Long An", + "tags": "Long An", + "level": 2, + "code": "LONGAN", + "id": 39 + }, + { + "parentId": 11923, + "type": "1", + "name": "Nam Định", + "tags": "Nam Dinh", + "level": 2, + "code": "NAMDINH", + "id": 40 + }, + { + "parentId": 11924, + "type": "1", + "name": "Nghệ An", + "tags": "Nghe An", + "level": 2, + "code": "NGHEAN", + "id": 41 + }, + { + "parentId": 11923, + "type": "1", + "name": "Ninh Bình", + "tags": "Ninh Binh", + "level": 2, + "code": "NINHBINH", + "id": 42 + }, + { + "parentId": 11924, + "type": "1", + "name": "Ninh Thuận", + "tags": "Ninh Thuan", + "level": 2, + "code": "NINHTHUAN", + "id": 43 + }, + { + "parentId": 11923, + "type": "1", + "name": "Phú Thọ", + "tags": "Phu Tho", + "level": 2, + "code": "PHUTHO", + "id": 44 + }, + { + "parentId": 11924, + "type": "1", + "name": "Phú Yên", + "tags": "Phu Yen", + "level": 2, + "code": "PHUYEN", + "id": 45 + }, + { + "parentId": 11924, + "type": "1", + "name": "Quảng Bình", + "tags": "Quang Binh", + "level": 2, + "code": "QUANGBINH", + "id": 46 + }, + { + "parentId": 11924, + "type": "1", + "name": "Quảng Nam", + "tags": "Quang Nam", + "level": 2, + "code": "QUANGNAM", + "id": 47 + }, + { + "parentId": 11924, + "type": "1", + "name": "Quảng Ngãi", + "tags": "Quang Ngai", + "level": 2, + "code": "QUANGNGAI", + "id": 48 + }, + { + "parentId": 11923, + "type": "1", + "name": "Quảng Ninh", + "tags": "Quang Ninh", + "level": 2, + "code": "QUANGNINH", + "id": 49 + }, + { + "parentId": 11924, + "type": "1", + "name": "Quảng Trị", + "tags": "Quang Tri", + "level": 2, + "code": "QUANGTRI", + "id": 50 + }, + { + "parentId": 11925, + "type": "1", + "name": "Sóc Trăng", + "tags": "Soc Trang", + "level": 2, + "code": "SOCTRANG", + "id": 51 + }, + { + "parentId": 11923, + "type": "1", + "name": "Sơn La", + "tags": "Son La", + "level": 2, + "code": "SONLA", + "id": 52 + }, + { + "parentId": 11927, + "type": "1", + "name": "Tây Ninh", + "tags": "Tay Ninh", + "level": 2, + "code": "TAYNINH", + "id": 53 + }, + { + "parentId": 11923, + "type": "1", + "name": "Thái Bình", + "tags": "Thai Binh", + "level": 2, + "code": "THAIBINH", + "id": 54 + }, + { + "parentId": 11923, + "type": "1", + "name": "Thái Nguyên", + "tags": "Thai Nguyen", + "level": 2, + "code": "THAINGUYEN", + "id": 55 + }, + { + "parentId": 11924, + "type": "1", + "name": "Thanh Hóa", + "tags": "Thanh Hoa", + "level": 2, + "code": "THANHHOA", + "id": 56 + }, + { + "parentId": 11924, + "type": "1", + "name": "Thừa Thiên - Huế", + "tags": "Thua Thien - Hue", + "level": 2, + "code": "HUE", + "id": 57 + }, + { + "parentId": 11925, + "type": "1", + "name": "Tiền Giang", + "tags": "Tien Giang", + "level": 2, + "code": "TIENGIANG", + "id": 58 + }, + { + "parentId": 11925, + "type": "1", + "name": "Trà Vinh", + "tags": "Tra Vinh", + "level": 2, + "code": "TRAVINH", + "id": 59 + }, + { + "parentId": 11923, + "type": "1", + "name": "Tuyên Quang", + "tags": "Tuyen Quang", + "level": 2, + "code": "TUYENQUANG", + "id": 60 + }, + { + "parentId": 11925, + "type": "1", + "name": "Vĩnh Long", + "tags": "Vinh Long", + "level": 2, + "code": "VINHLONG", + "id": 61 + }, + { + "parentId": 11923, + "type": "1", + "name": "Vĩnh Phúc", + "tags": "Vinh Phuc", + "level": 2, + "code": "VINHPHUC", + "id": 62 + }, + { + "parentId": 11923, + "type": "1", + "name": "Yên Bái", + "tags": "Yen Bai", + "level": 2, + "code": "YENBAI", + "id": 63 + }, + { + "parentId": 1, + "type": "2", + "name": "Long Xuyên", + "tags": "Long Xuyen", + "level": 3, + "code": "LONGXUYEN", + "id": 64 + }, + { + "parentId": 1, + "type": "2", + "name": "Châu Đốc", + "tags": "Chau Doc", + "level": 3, + "code": "CHAUDOC", + "id": 65 + }, + { + "parentId": 1, + "type": "2", + "name": "Tịnh Biên", + "tags": "Tinh Bien", + "level": 3, + "code": "TINHBIEN", + "id": 73 + }, + { + "parentId": 1, + "type": "2", + "name": "Tri Tôn", + "tags": "Tri Ton", + "level": 3, + "code": "TRITON", + "id": 74 + }, + { + "parentId": 9, + "type": "2", + "name": "Quy Nhơn", + "tags": "Quy Nhon", + "level": 3, + "code": "QUINHON", + "id": 134 + }, + { + "parentId": 9, + "type": "2", + "name": "An Nhơn", + "tags": "An Nhon", + "level": 3, + "code": "ANNHON", + "id": 135 + }, + { + "parentId": 11, + "type": "2", + "name": "Phan Thiết", + "tags": "Phan Thiet", + "level": 3, + "code": "PHANTHIET", + "id": 156 + }, + { + "parentId": 12, + "type": "2", + "name": "Năm Căn", + "tags": "Nam Can", + "level": 3, + "code": "NAMCAN", + "id": 168 + }, + { + "parentId": 14, + "type": "2", + "name": "Thốt Nốt", + "tags": "Thot Not", + "level": 3, + "code": "THOTNOT", + "id": 192 + }, + { + "parentId": 16, + "type": "2", + "name": "Buôn Ma Thuột", + "tags": "Buon Ma Thuot", + "level": 3, + "code": "BUONMATHUOT", + "id": 205 + }, + { + "parentId": 17, + "type": "2", + "name": "Cư Jút", + "tags": "Cu Jut", + "level": 3, + "code": "CUJUT", + "id": 223 + }, + { + "parentId": 19, + "type": "2", + "name": "Cao Lãnh", + "tags": "Cao Lanh", + "level": 3, + "code": "CAOLANH", + "id": 238 + }, + { + "parentId": 19, + "type": "2", + "name": "Sa Đéc", + "tags": "Sa Dec", + "level": 3, + "code": "SADEC", + "id": 239 + }, + { + "parentId": 19, + "type": "2", + "name": "Hồng Ngự", + "tags": "Hong Ngu", + "level": 3, + "code": "HONGNGU", + "id": 240 + }, + { + "parentId": 29, + "type": "2", + "name": "Vị Thanh", + "tags": "Vi Thanh", + "level": 3, + "code": "VITHANH", + "id": 371 + }, + { + "parentId": 32, + "type": "2", + "name": "Nha Trang", + "tags": "Nha Trang", + "level": 3, + "code": "NHATRANG", + "id": 411 + }, + { + "parentId": 32, + "type": "2", + "name": "Cam Ranh", + "tags": "Cam Ranh", + "level": 3, + "code": "CAMRANH", + "id": 412 + }, + { + "parentId": 32, + "type": "2", + "name": "Ninh Hòa", + "tags": "Ninh Hoa", + "level": 3, + "code": "NINHHOA", + "id": 413 + }, + { + "parentId": 33, + "type": "2", + "name": "Rạch Giá", + "tags": "Rach Gia", + "level": 3, + "code": "RACHGIA", + "id": 420 + }, + { + "parentId": 33, + "type": "2", + "name": "Hà Tiên", + "tags": "Ha Tien", + "level": 3, + "code": "HATIEN", + "id": 421 + }, + { + "parentId": 33, + "type": "2", + "name": "Vĩnh Thuận", + "tags": "Vinh Thuan", + "level": 3, + "code": "VINHTHUAN", + "id": 432 + }, + { + "parentId": 38, + "type": "2", + "name": "Đà Lạt", + "tags": "Da Lat", + "level": 3, + "code": "DALAT", + "id": 474 + }, + { + "parentId": 38, + "type": "2", + "name": "Bảo Lộc", + "tags": "Bao Loc", + "level": 3, + "code": "BAOLOC", + "id": 475 + }, + { + "parentId": 38, + "type": "2", + "name": "Đơn Dương", + "tags": "Don Duong", + "level": 3, + "code": "DONDUONG", + "id": 477 + }, + { + "parentId": 43, + "type": "2", + "name": "Phan Rang-Tháp Chàm", + "tags": "Phan Rang-Thap Cham", + "level": 3, + "code": "PHANRANG", + "id": 539 + }, + { + "parentId": 45, + "type": "2", + "name": "Tuy Hoà", + "tags": "Tuy Hoa", + "level": 3, + "code": "TUYHOA", + "id": 559 + }, + { + "parentId": 47, + "type": "2", + "name": "Đại Lộc", + "tags": "Dai Loc", + "level": 3, + "code": "DAILOC", + "id": 581 + }, + { + "parentId": 51, + "type": "2", + "name": "Vĩnh Châu ", + "tags": "Vinh Chau ", + "level": 3, + "code": "VINHCHAU", + "id": 633 + }, + { + "parentId": 51, + "type": "2", + "name": "Ngã Năm", + "tags": "Nga Nam", + "level": 3, + "code": "NGANAM", + "id": 634 + }, + { + "parentId": 61, + "type": "2", + "name": "Bình Minh", + "tags": "Binh Minh", + "level": 3, + "code": "BINHMINH", + "id": 743 + }, + { + "parentId": 61, + "type": "2", + "name": "Trà Ôn", + "tags": "Tra On", + "level": 3, + "code": "TRAON", + "id": 748 + }, + { + "parentId": 84, + "type": "3", + "name": "Phường Hộ Phòng", + "tags": "Phuong Ho Phong", + "level": 4, + "code": "HOPHONG", + "id": 1353 + }, + { + "parentId": 156, + "type": "3", + "name": "Phường Mũi Né", + "tags": "Phuong Mui Ne", + "level": 4, + "code": "MUINE", + "id": 2008 + }, + { + "parentId": 638, + "type": "3", + "name": "Thị Trấn Đại Ngãi", + "tags": "Thi Tran Dai Ngai", + "level": 4, + "code": "DAINGAI", + "id": 11789 + } +] \ No newline at end of file diff --git a/app/templates/index.html b/templates/index.html similarity index 100% rename from app/templates/index.html rename to templates/index.html diff --git a/utils/__pycache__/health_check.cpython-310.pyc b/utils/__pycache__/health_check.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d8533158fa5d280e78dfce27537154d88dfac6b Binary files /dev/null and b/utils/__pycache__/health_check.cpython-310.pyc differ diff --git a/utils/__pycache__/openapi.cpython-310.pyc b/utils/__pycache__/openapi.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d305c79ec49f64e0928b8ac86fb9dbcea8949397 Binary files /dev/null and b/utils/__pycache__/openapi.cpython-310.pyc differ diff --git a/utils/__pycache__/serializers.cpython-310.pyc b/utils/__pycache__/serializers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fc72c2fad9ce8c65e5ea565bb62f87911859669 Binary files /dev/null and b/utils/__pycache__/serializers.cpython-310.pyc differ diff --git a/utils/__pycache__/trace_id.cpython-310.pyc b/utils/__pycache__/trace_id.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58c62cdf5ae558f31daa5a8a780db8bb16c60e92 Binary files /dev/null and b/utils/__pycache__/trace_id.cpython-310.pyc differ diff --git a/utils/health_check.py b/utils/health_check.py new file mode 100644 index 0000000000000000000000000000000000000000..979f452818aeebd61f92f4ab10652d97aa30924a --- /dev/null +++ b/utils/health_check.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from math import ceil + +from fastapi import FastAPI, Request, Response +from fastapi.routing import APIRoute + +from common.exception import errors + + +def ensure_unique_route_names(app: FastAPI) -> None: + """ + + :param app: FastAPI + :return: + """ + temp_routes = set() + for route in app.routes: + if isinstance(route, APIRoute): + if route.name in temp_routes: + raise ValueError(f'Non-unique route name: {route.name}') + temp_routes.add(route.name) + + +async def http_limit_callback(request: Request, response: Response, expire: int) -> None: + """ + + :param request: FastAPI + :param response: FastAPI + :param expire: + :return: + """ + expires = ceil(expire / 1000) + raise errors.HTTPError(code=429, msg='The request is too frequent, please try again later', headers={'Retry-After': str(expires)}) diff --git a/utils/openapi.py b/utils/openapi.py new file mode 100644 index 0000000000000000000000000000000000000000..b5db5c130850df95e10928725e86cff7fba41403 --- /dev/null +++ b/utils/openapi.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import FastAPI +from fastapi.routing import APIRoute + + +def simplify_operation_ids(app: FastAPI) -> None: + """ + 简化操作 ID,以便生成的客户端具有更简单的 API 函数名称 + + :param app: FastAPI 应用实例 + :return: + """ + for route in app.routes: + if isinstance(route, APIRoute): + route.operation_id = route.name diff --git a/utils/serializers.py b/utils/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..948e6e477e4dc704ce16b4b3421ba34a58fcb728 --- /dev/null +++ b/utils/serializers.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Any + +from msgspec import json +from starlette.responses import JSONResponse + +class MsgSpecJSONResponse(JSONResponse): + """ + MsgSpec JSON + """ + + def render(self, content: Any) -> bytes: + return json.encode(content) diff --git a/utils/trace_id.py b/utils/trace_id.py new file mode 100644 index 0000000000000000000000000000000000000000..b49736619cd9482ca4a07b6fb47e8c14fe825103 --- /dev/null +++ b/utils/trace_id.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import Request + +from core.conf import settings + + +def get_request_trace_id(request: Request) -> str: + return request.headers.get(settings.TRACE_ID_REQUEST_HEADER_KEY) or settings.LOG_CID_DEFAULT_VALUE