Spaces:
Sleeping
Sleeping
OnlyBiggg
commited on
Commit
·
5564ecb
1
Parent(s):
456b94f
'refactor'
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +0 -1
- .gitignore +1 -0
- Dockerfile +4 -2
- __pycache__/app.cpython-39.pyc +0 -0
- __pycache__/main.cpython-39.pyc +0 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-310.pyc +0 -0
- app/__pycache__/router.cpython-310.pyc +0 -0
- app/busbooking-451909-aa92e337868d.json +0 -13
- app/core/config.py +0 -9
- app/dialogflow/api/__init__.py +0 -0
- app/dialogflow/api/__pycache__/__init__.cpython-310.pyc +0 -0
- app/dialogflow/api/__pycache__/router.cpython-310.pyc +0 -0
- app/{api → dialogflow/api}/__pycache__/routes.cpython-39.pyc +0 -0
- app/dialogflow/api/router.py +10 -0
- app/dialogflow/api/v1/__init__.py +0 -0
- app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc +0 -0
- app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc +0 -0
- app/{api/routes.py → dialogflow/api/v1/dialogflow.py} +68 -62
- app/dialogflow/schemas/__init__.py +0 -0
- app/dialogflow/services/__init__.py +0 -0
- app/dialogflow/services/__pycache__/__init__.cpython-310.pyc +0 -0
- app/{services → dialogflow/services}/__pycache__/api.cpython-39.pyc +0 -0
- app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc +0 -0
- app/{services → dialogflow/services}/__pycache__/external_api.cpython-39.pyc +0 -0
- app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc +0 -0
- app/dialogflow/services/dialog_service.py +57 -0
- app/dialogflow/services/origin_codes.py +4 -0
- app/router.py +9 -0
- app/services/api.py +0 -61
- app/types/__pycache__/Respone.cpython-39.pyc +0 -0
- app/utils/__pycache__/constants.cpython-39.pyc +0 -0
- app/utils/constants.py +0 -5
- common/__init__.py +0 -0
- common/__pycache__/__init__.cpython-310.pyc +0 -0
- common/__pycache__/__init__.cpython-39.pyc +0 -0
- common/__pycache__/log.cpython-310.pyc +0 -0
- common/__pycache__/schema.cpython-310.pyc +0 -0
- common/exception/__init__.py +2 -0
- common/exception/__pycache__/__init__.cpython-310.pyc +0 -0
- common/exception/__pycache__/__init__.cpython-39.pyc +0 -0
- common/exception/__pycache__/errors.cpython-310.pyc +0 -0
- common/exception/__pycache__/errors.cpython-39.pyc +0 -0
- common/exception/__pycache__/exception_handler.cpython-310.pyc +0 -0
- common/exception/__pycache__/exception_handler.cpython-39.pyc +0 -0
- common/exception/errors.py +89 -0
- common/exception/exception_handler.py +199 -0
- common/external/__pycache__/external_api.cpython-310.pyc +0 -0
- common/external/external_api.py +83 -0
- common/log.py +106 -0
.env
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
API_BASE_URL=https://api-dev.futabus.vn
|
|
|
|
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
.env
|
Dockerfile
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
-
FROM python:3.
|
2 |
|
3 |
-
ENV API_BASE_URL https://api-dev.futabus.vn
|
|
|
|
|
4 |
|
5 |
COPY . .
|
6 |
|
|
|
1 |
+
FROM python:3.10.4
|
2 |
|
3 |
+
ENV API_BASE_URL 'https://api-dev.futabus.vn'
|
4 |
+
ENV API_ACCESS_TOKEN 'https://api-dev.futabus.vn/identity/api/token/anonymous-token'
|
5 |
+
ENV ENVIRONMENT 'dev'
|
6 |
|
7 |
COPY . .
|
8 |
|
__pycache__/app.cpython-39.pyc
DELETED
Binary file (435 Bytes)
|
|
__pycache__/main.cpython-39.pyc
DELETED
Binary file (443 Bytes)
|
|
app/__init__.py
ADDED
File without changes
|
app/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (161 Bytes). View file
|
|
app/__pycache__/router.cpython-310.pyc
ADDED
Binary file (311 Bytes). View file
|
|
app/busbooking-451909-aa92e337868d.json
DELETED
@@ -1,13 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"type": "service_account",
|
3 |
-
"project_id": "busbooking-451909",
|
4 |
-
"private_key_id": "aa92e337868d77340f1e1bc3d0119738f48b8a49",
|
5 |
-
"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",
|
6 |
-
"client_email": "[email protected]",
|
7 |
-
"client_id": "111019976759266480456",
|
8 |
-
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
9 |
-
"token_uri": "https://oauth2.googleapis.com/token",
|
10 |
-
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
11 |
-
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/chatbot%40busbooking-451909.iam.gserviceaccount.com",
|
12 |
-
"universe_domain": "googleapis.com"
|
13 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/core/config.py
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from dotenv import load_dotenv
|
3 |
-
|
4 |
-
load_dotenv() # Load biến môi trường từ file .env
|
5 |
-
|
6 |
-
class Settings:
|
7 |
-
API_BASE_URL = os.getenv("API_BASE_URL") # API gốc (backend dev)
|
8 |
-
|
9 |
-
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/dialogflow/api/__init__.py
ADDED
File without changes
|
app/dialogflow/api/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (176 Bytes). View file
|
|
app/dialogflow/api/__pycache__/router.cpython-310.pyc
ADDED
Binary file (416 Bytes). View file
|
|
app/{api → dialogflow/api}/__pycache__/routes.cpython-39.pyc
RENAMED
File without changes
|
app/dialogflow/api/router.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
from fastapi import APIRouter
|
4 |
+
|
5 |
+
from app.dialogflow.api.v1.dialogflow import router as dialogflow_router
|
6 |
+
from core.conf import settings
|
7 |
+
|
8 |
+
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
9 |
+
|
10 |
+
v1.include_router(dialogflow_router)
|
app/dialogflow/api/v1/__init__.py
ADDED
File without changes
|
app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (179 Bytes). View file
|
|
app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc
ADDED
Binary file (9.93 kB). View file
|
|
app/{api/routes.py → dialogflow/api/v1/dialogflow.py}
RENAMED
@@ -3,67 +3,34 @@ from fastapi import FastAPI, APIRouter, HTTPException, Request, Response # type:
|
|
3 |
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse # type: ignore
|
4 |
from datetime import datetime, timedelta
|
5 |
from fastapi.templating import Jinja2Templates
|
|
|
6 |
|
7 |
|
8 |
-
from
|
9 |
-
from app.
|
10 |
-
from
|
11 |
router = APIRouter()
|
12 |
|
13 |
-
templates = Jinja2Templates(directory="
|
14 |
-
|
15 |
-
def to_datetime_from_Dialogflow(time: dict):
|
16 |
-
date_time = datetime(int(time["year"]), int(time["month"]), int(time["day"]))
|
17 |
-
return date_time
|
18 |
-
|
19 |
-
def process_dates_to_timestamp(from_time: datetime = None, to_time: datetime = None):
|
20 |
-
if to_time is None and from_time is not None:
|
21 |
-
to_time = from_time.replace(hour=23, minute=59, second=59)
|
22 |
-
|
23 |
-
if from_time is None:
|
24 |
-
today = datetime.today().date()
|
25 |
-
from_time = datetime.combine(today, datetime.min.time())
|
26 |
-
to_time = datetime.combine(today, datetime.max.time()) - timedelta(microseconds=1)
|
27 |
-
|
28 |
-
return int(from_time.timestamp()) * 1000 , int(to_time.timestamp()) * 1000
|
29 |
-
def get_param_from_dialogflow(body: any):
|
30 |
-
session_info = body.get("sessionInfo", {})
|
31 |
-
parameters = session_info.get("parameters", {}) if isinstance(session_info.get("parameters"), dict) else {}
|
32 |
-
raw_date = parameters.get("date")
|
33 |
-
if raw_date is not None:
|
34 |
-
raw_date = to_datetime_from_Dialogflow(raw_date)
|
35 |
-
raw_departure_city = parameters.get("departure_city")
|
36 |
-
raw_destination_city = parameters.get("destination_city")
|
37 |
-
raw_ticket_number = parameters.get("ticket_number")
|
38 |
-
raw_time_of_day = parameters.get("time_of_day")
|
39 |
-
return raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day
|
40 |
-
|
41 |
-
async def search_route_ids_from_province(departure_code: str, destination_code: str):
|
42 |
-
response = await api.get(f'/metadata/office/routes?DestCode={destination_code}&OriginCode={departure_code}')
|
43 |
-
route_ids = []
|
44 |
-
if isinstance(response, list): # Kiểm tra nếu data là danh sách
|
45 |
-
route_ids = [route.get("routeId", -1) for route in response]
|
46 |
-
|
47 |
-
return route_ids
|
48 |
|
49 |
@router.post('/routes')
|
50 |
async def route(request: Request):
|
51 |
body = await request.json()
|
52 |
-
raw_departure_city, raw_destination_city, raw_ticket_number , raw_date, _ = get_param_from_dialogflow(body)
|
53 |
|
54 |
|
55 |
ticket_count = int(raw_ticket_number) if raw_ticket_number else 1
|
56 |
|
57 |
if raw_date is None:
|
58 |
-
from_time, to_time = process_dates_to_timestamp()
|
59 |
date = datetime.today().date().strftime('%m-%d-%Y')
|
60 |
else:
|
61 |
date = raw_date.strftime('%m-%d-%Y')
|
62 |
-
from_time, to_time = process_dates_to_timestamp(raw_date)
|
63 |
-
departure_code =
|
64 |
-
destination_code =
|
65 |
-
route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code)
|
66 |
-
route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code)
|
67 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
68 |
payload = {
|
69 |
"from_time": from_time,
|
@@ -128,16 +95,16 @@ async def route(request: Request):
|
|
128 |
@router.post('/price')
|
129 |
async def price(request: Request):
|
130 |
body = await request.json()
|
131 |
-
raw_departure_city, raw_destination_city, _, raw_date, _ = get_param_from_dialogflow(body)
|
132 |
|
133 |
if raw_date is None:
|
134 |
-
from_time, to_time = process_dates_to_timestamp()
|
135 |
-
from_time, to_time = process_dates_to_timestamp(raw_date)
|
136 |
|
137 |
-
departure_code =
|
138 |
-
destination_code =
|
139 |
-
route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code)
|
140 |
-
route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code)
|
141 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
142 |
payload = {
|
143 |
"from_time": from_time,
|
@@ -193,17 +160,17 @@ async def price(request: Request):
|
|
193 |
@router.post('/trip/list')
|
194 |
async def booking_trip(request: Request) -> Response:
|
195 |
body = await request.json()
|
196 |
-
raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day = get_param_from_dialogflow(body)
|
197 |
|
198 |
date = raw_date.strftime('%m-%d-%Y')
|
199 |
-
from_time, to_time = process_dates_to_timestamp(raw_date)
|
200 |
ticket_count = int(raw_ticket_number) if raw_ticket_number else 1
|
201 |
|
202 |
-
departure_code =
|
203 |
-
destination_code =
|
204 |
|
205 |
-
route_dep_to_des = await search_route_ids_from_province(departure_code,destination_code)
|
206 |
-
route_des_to_dep = await search_route_ids_from_province(destination_code,departure_code)
|
207 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
208 |
payload = {
|
209 |
"from_time": from_time,
|
@@ -233,7 +200,9 @@ async def booking_trip(request: Request) -> Response:
|
|
233 |
"id": trip["id"],
|
234 |
"departure_date": trip["raw_departure_date"],
|
235 |
"departure_time": trip["raw_departure_time"],
|
236 |
-
"kind": trip["seat_type_name"]
|
|
|
|
|
237 |
})
|
238 |
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))]
|
239 |
payload={
|
@@ -301,13 +270,16 @@ async def is_valid_select_trip(request: Request) -> Response:
|
|
301 |
if trip["route_name"] == raw_input:
|
302 |
route_id = int(trip["route_id"])
|
303 |
kind = trip["kind"]
|
|
|
304 |
break
|
305 |
|
306 |
parameters = {
|
307 |
"is_valid_trip": True,
|
308 |
"route_name": raw_input,
|
309 |
"kind": kind,
|
310 |
-
"route_id": route_id
|
|
|
|
|
311 |
}
|
312 |
else:
|
313 |
parameters = {
|
@@ -333,7 +305,7 @@ async def time_trip(request: Request) -> Response:
|
|
333 |
parameters = {
|
334 |
"time_list": time_list
|
335 |
}
|
336 |
-
text = [f"Quý khách lựa chọn thời gian chuyến
|
337 |
payload={
|
338 |
"richContent": [
|
339 |
[
|
@@ -368,7 +340,7 @@ async def is_valid_select_time(request: Request) -> Response:
|
|
368 |
"is_valid_time": True,
|
369 |
"departure_time": raw_input
|
370 |
}
|
371 |
-
text = [f'{raw_input} | {route_name}']
|
372 |
else:
|
373 |
parameters = {
|
374 |
"is_valid_time": False
|
@@ -379,6 +351,40 @@ async def is_valid_select_time(request: Request) -> Response:
|
|
379 |
print(e)
|
380 |
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ợ."])
|
381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
@router.get("/")
|
383 |
def home():
|
384 |
return "Hello World!"
|
|
|
3 |
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse # type: ignore
|
4 |
from datetime import datetime, timedelta
|
5 |
from fastapi.templating import Jinja2Templates
|
6 |
+
from app.dialogflow.services.dialog_service import dialog_service
|
7 |
|
8 |
|
9 |
+
from common.external.external_api import api
|
10 |
+
from app.dialogflow.services.origin_codes import origin_codes
|
11 |
+
from common.response.respone_dialogflow import DialogFlowResponseAPI
|
12 |
router = APIRouter()
|
13 |
|
14 |
+
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
@router.post('/routes')
|
17 |
async def route(request: Request):
|
18 |
body = await request.json()
|
19 |
+
raw_departure_city, raw_destination_city, raw_ticket_number , raw_date, _ = dialog_service.get_param_from_dialogflow(body)
|
20 |
|
21 |
|
22 |
ticket_count = int(raw_ticket_number) if raw_ticket_number else 1
|
23 |
|
24 |
if raw_date is None:
|
25 |
+
from_time, to_time = dialog_service.process_dates_to_timestamp()
|
26 |
date = datetime.today().date().strftime('%m-%d-%Y')
|
27 |
else:
|
28 |
date = raw_date.strftime('%m-%d-%Y')
|
29 |
+
from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date)
|
30 |
+
departure_code = origin_codes.get(raw_departure_city)
|
31 |
+
destination_code = origin_codes.get(raw_destination_city)
|
32 |
+
route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code)
|
33 |
+
route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code)
|
34 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
35 |
payload = {
|
36 |
"from_time": from_time,
|
|
|
95 |
@router.post('/price')
|
96 |
async def price(request: Request):
|
97 |
body = await request.json()
|
98 |
+
raw_departure_city, raw_destination_city, _, raw_date, _ = dialog_service.get_param_from_dialogflow(body)
|
99 |
|
100 |
if raw_date is None:
|
101 |
+
from_time, to_time = dialog_service.process_dates_to_timestamp()
|
102 |
+
from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date)
|
103 |
|
104 |
+
departure_code = origin_codes.get(raw_departure_city)
|
105 |
+
destination_code = origin_codes.get(raw_destination_city)
|
106 |
+
route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code)
|
107 |
+
route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code)
|
108 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
109 |
payload = {
|
110 |
"from_time": from_time,
|
|
|
160 |
@router.post('/trip/list')
|
161 |
async def booking_trip(request: Request) -> Response:
|
162 |
body = await request.json()
|
163 |
+
raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day = dialog_service.get_param_from_dialogflow(body)
|
164 |
|
165 |
date = raw_date.strftime('%m-%d-%Y')
|
166 |
+
from_time, to_time = dialog_service.process_dates_to_timestamp(raw_date)
|
167 |
ticket_count = int(raw_ticket_number) if raw_ticket_number else 1
|
168 |
|
169 |
+
departure_code = origin_codes.get(raw_departure_city)
|
170 |
+
destination_code = origin_codes.get(raw_destination_city)
|
171 |
|
172 |
+
route_dep_to_des = await dialog_service.search_route_ids_from_province(departure_code,destination_code)
|
173 |
+
route_des_to_dep = await dialog_service.search_route_ids_from_province(destination_code,departure_code)
|
174 |
routes_ids = list(set(route_dep_to_des + route_des_to_dep))
|
175 |
payload = {
|
176 |
"from_time": from_time,
|
|
|
200 |
"id": trip["id"],
|
201 |
"departure_date": trip["raw_departure_date"],
|
202 |
"departure_time": trip["raw_departure_time"],
|
203 |
+
"kind": trip["seat_type_name"],
|
204 |
+
"way_id": trip["way_id"]
|
205 |
+
|
206 |
})
|
207 |
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))]
|
208 |
payload={
|
|
|
270 |
if trip["route_name"] == raw_input:
|
271 |
route_id = int(trip["route_id"])
|
272 |
kind = trip["kind"]
|
273 |
+
way_id = trip["way_id"]
|
274 |
break
|
275 |
|
276 |
parameters = {
|
277 |
"is_valid_trip": True,
|
278 |
"route_name": raw_input,
|
279 |
"kind": kind,
|
280 |
+
"route_id": route_id,
|
281 |
+
"way_id": way_id,
|
282 |
+
|
283 |
}
|
284 |
else:
|
285 |
parameters = {
|
|
|
305 |
parameters = {
|
306 |
"time_list": time_list
|
307 |
}
|
308 |
+
text = [f"Quý khách lựa chọn thời gian chuyến {route_name}\n" + " | ".join(map(str, time_list))]
|
309 |
payload={
|
310 |
"richContent": [
|
311 |
[
|
|
|
340 |
"is_valid_time": True,
|
341 |
"departure_time": raw_input
|
342 |
}
|
343 |
+
text = [f' Quý khách chọn chuyến {raw_input} | {route_name}']
|
344 |
else:
|
345 |
parameters = {
|
346 |
"is_valid_time": False
|
|
|
351 |
print(e)
|
352 |
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ợ."])
|
353 |
|
354 |
+
|
355 |
+
@router.post('/trip/seats')
|
356 |
+
async def seats_trip(request: Request) -> Response:
|
357 |
+
try:
|
358 |
+
body = await request.json()
|
359 |
+
session_info = body.get("sessionInfo", {})
|
360 |
+
parameters = session_info.get("parameters")
|
361 |
+
|
362 |
+
trip_id: int = parameters.get("trip_id", None)
|
363 |
+
route_id: int = parameters.get("route_id", None)
|
364 |
+
departure_date: str = parameters.get("departure_date", None)
|
365 |
+
departure_time: str = parameters.get("departure_time", None)
|
366 |
+
kind: str = parameters.get("kind", None)
|
367 |
+
seats_empty, seats = await dialog_service.seats_trip(route_id, trip_id, departure_date, departure_time, kind)
|
368 |
+
text=["Vui lòng chọn ghế"]
|
369 |
+
payload={
|
370 |
+
"richContent": [
|
371 |
+
[
|
372 |
+
{
|
373 |
+
"type": "chips",
|
374 |
+
"options": [
|
375 |
+
{"text": seat["chair"]} for seat in (seats_empty)
|
376 |
+
]
|
377 |
+
}
|
378 |
+
]
|
379 |
+
]
|
380 |
+
}
|
381 |
+
parameters = {
|
382 |
+
"seats": seats
|
383 |
+
}
|
384 |
+
return DialogFlowResponseAPI(text=text, payload=payload, parameters=parameters)
|
385 |
+
except Exception as e:
|
386 |
+
print(e)
|
387 |
+
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ợ."])
|
388 |
@router.get("/")
|
389 |
def home():
|
390 |
return "Hello World!"
|
app/dialogflow/schemas/__init__.py
ADDED
File without changes
|
app/dialogflow/services/__init__.py
ADDED
File without changes
|
app/dialogflow/services/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (181 Bytes). View file
|
|
app/{services → dialogflow/services}/__pycache__/api.cpython-39.pyc
RENAMED
File without changes
|
app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc
ADDED
Binary file (3.14 kB). View file
|
|
app/{services → dialogflow/services}/__pycache__/external_api.cpython-39.pyc
RENAMED
File without changes
|
app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc
ADDED
Binary file (352 Bytes). View file
|
|
app/dialogflow/services/dialog_service.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from common.external.external_api import api
|
4 |
+
|
5 |
+
class DialogService:
|
6 |
+
@staticmethod
|
7 |
+
def to_datetime_from_Dialogflow(time: dict):
|
8 |
+
date_time = datetime(int(time["year"]), int(time["month"]), int(time["day"]))
|
9 |
+
return date_time
|
10 |
+
|
11 |
+
def process_dates_to_timestamp(from_time: datetime = None, to_time: datetime = None):
|
12 |
+
if to_time is None and from_time is not None:
|
13 |
+
to_time = from_time.replace(hour=23, minute=59, second=59)
|
14 |
+
|
15 |
+
if from_time is None:
|
16 |
+
today = datetime.today().date()
|
17 |
+
from_time = datetime.combine(today, datetime.min.time())
|
18 |
+
to_time = datetime.combine(today, datetime.max.time()) - timedelta(microseconds=1)
|
19 |
+
|
20 |
+
return int(from_time.timestamp()) * 1000 , int(to_time.timestamp()) * 1000
|
21 |
+
def get_param_from_dialogflow(self, body: any):
|
22 |
+
session_info = body.get("sessionInfo", {})
|
23 |
+
parameters = session_info.get("parameters", {}) if isinstance(session_info.get("parameters"), dict) else {}
|
24 |
+
raw_date = parameters.get("date")
|
25 |
+
if raw_date is not None:
|
26 |
+
raw_date = self.to_datetime_from_Dialogflow(raw_date)
|
27 |
+
raw_departure_city = parameters.get("departure_city")
|
28 |
+
raw_destination_city = parameters.get("destination_city")
|
29 |
+
raw_ticket_number = parameters.get("ticket_number")
|
30 |
+
raw_time_of_day = parameters.get("time_of_day")
|
31 |
+
return raw_departure_city, raw_destination_city, raw_ticket_number, raw_date, raw_time_of_day
|
32 |
+
|
33 |
+
async def search_route_ids_from_province(departure_code: str, destination_code: str):
|
34 |
+
response = await api.get(f'/metadata/office/routes?DestCode={destination_code}&OriginCode={departure_code}')
|
35 |
+
route_ids = []
|
36 |
+
if isinstance(response, list):
|
37 |
+
route_ids = [route.get("routeId", -1) for route in response]
|
38 |
+
return route_ids
|
39 |
+
|
40 |
+
|
41 |
+
async def seats_trip(route_id: int, trip_id:int, departure_date: str, departure_time: str, kind: str):
|
42 |
+
try:
|
43 |
+
params = {
|
44 |
+
"departureDate": departure_date,
|
45 |
+
"departureTime": departure_time,
|
46 |
+
"kind": kind,
|
47 |
+
}
|
48 |
+
response = api.get(api_base="https://api-busline-dev.vato.vn/api", endpoint=f"/buslines/futa/booking/seats/{route_id}/{trip_id}" , params=params)
|
49 |
+
seats = response["data"]
|
50 |
+
seats_empty = [ seat for seat in seats if seat["bookStatus"] == 0 ]
|
51 |
+
return seats_empty, seats
|
52 |
+
except Exception as e:
|
53 |
+
print(e)
|
54 |
+
raise Exception("Error fetching seats data")
|
55 |
+
|
56 |
+
dialog_service: DialogService = DialogService()
|
57 |
+
|
app/dialogflow/services/origin_codes.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
with open("static/files/code_province.json", "r", encoding="utf-8") as file:
|
4 |
+
origin_codes = json.load(file)
|
app/router.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
from fastapi import APIRouter
|
4 |
+
|
5 |
+
from app.dialogflow.api.router import v1 as dialogflow_v1
|
6 |
+
|
7 |
+
router = APIRouter()
|
8 |
+
|
9 |
+
router.include_router(dialogflow_v1)
|
app/services/api.py
DELETED
@@ -1,61 +0,0 @@
|
|
1 |
-
import httpx
|
2 |
-
from app.core.config import settings
|
3 |
-
from typing import Optional, Dict, Any
|
4 |
-
|
5 |
-
API_BASE_URL = settings.API_BASE_URL
|
6 |
-
|
7 |
-
async def get(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None):
|
8 |
-
"""Gọi API GET"""
|
9 |
-
url = f"{API_BASE_URL}{endpoint}"
|
10 |
-
headers = headers or {}
|
11 |
-
async with httpx.AsyncClient() as client:
|
12 |
-
try:
|
13 |
-
response = await client.get(url, headers=headers, params=params)
|
14 |
-
response.raise_for_status()
|
15 |
-
return response.json()
|
16 |
-
except httpx.HTTPStatusError as http_err:
|
17 |
-
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
18 |
-
except Exception as err:
|
19 |
-
return {"error": f"Request failed: {str(err)}"}
|
20 |
-
|
21 |
-
async def post(endpoint: str , payload: Dict[str, Any] = None,headers: Optional[Dict[str, str]] = None):
|
22 |
-
"""Gọi API POST"""
|
23 |
-
url = f"{API_BASE_URL}{endpoint}"
|
24 |
-
headers = headers or {"Content-Type": "application/json"}
|
25 |
-
async with httpx.AsyncClient() as client:
|
26 |
-
try:
|
27 |
-
response = await client.post(url, json=payload, headers=headers)
|
28 |
-
response.raise_for_status()
|
29 |
-
return response.json()
|
30 |
-
except httpx.HTTPStatusError as http_err:
|
31 |
-
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
32 |
-
except Exception as err:
|
33 |
-
return {"error": f"Request failed: {str(err)}"}
|
34 |
-
|
35 |
-
async def put_api(endpoint: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None):
|
36 |
-
"""Gọi API PUT"""
|
37 |
-
url = f"{API_BASE_URL}{endpoint}"
|
38 |
-
headers = headers or {"Content-Type": "application/json"}
|
39 |
-
async with httpx.AsyncClient() as client:
|
40 |
-
try:
|
41 |
-
response = await client.put(url, json=payload, headers=headers)
|
42 |
-
response.raise_for_status()
|
43 |
-
return response.json()
|
44 |
-
except httpx.HTTPStatusError as http_err:
|
45 |
-
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
46 |
-
except Exception as err:
|
47 |
-
return {"error": f"Request failed: {str(err)}"}
|
48 |
-
|
49 |
-
async def delete_api(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None):
|
50 |
-
"""Gọi API DELETE"""
|
51 |
-
url = f"{API_BASE_URL}{endpoint}"
|
52 |
-
headers = headers or {}
|
53 |
-
async with httpx.AsyncClient() as client:
|
54 |
-
try:
|
55 |
-
response = await client.delete(url, headers=headers, params=params)
|
56 |
-
response.raise_for_status()
|
57 |
-
return response.json()
|
58 |
-
except httpx.HTTPStatusError as http_err:
|
59 |
-
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
60 |
-
except Exception as err:
|
61 |
-
return {"error": f"Request failed: {str(err)}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/types/__pycache__/Respone.cpython-39.pyc
DELETED
Binary file (1.89 kB)
|
|
app/utils/__pycache__/constants.cpython-39.pyc
DELETED
Binary file (320 Bytes)
|
|
app/utils/constants.py
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import json
|
2 |
-
|
3 |
-
# Đọc dữ liệu từ file JSON
|
4 |
-
with open("app/utils/code_province.json", "r", encoding="utf-8") as file:
|
5 |
-
code_province = json.load(file)
|
|
|
|
|
|
|
|
|
|
|
|
common/__init__.py
ADDED
File without changes
|
common/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (164 Bytes). View file
|
|
common/__pycache__/__init__.cpython-39.pyc
ADDED
Binary file (162 Bytes). View file
|
|
common/__pycache__/log.cpython-310.pyc
ADDED
Binary file (3.05 kB). View file
|
|
common/__pycache__/schema.cpython-310.pyc
ADDED
Binary file (7.91 kB). View file
|
|
common/exception/__init__.py
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
common/exception/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (174 Bytes). View file
|
|
common/exception/__pycache__/__init__.cpython-39.pyc
ADDED
Binary file (172 Bytes). View file
|
|
common/exception/__pycache__/errors.cpython-310.pyc
ADDED
Binary file (4.14 kB). View file
|
|
common/exception/__pycache__/errors.cpython-39.pyc
ADDED
Binary file (4.42 kB). View file
|
|
common/exception/__pycache__/exception_handler.cpython-310.pyc
ADDED
Binary file (6.09 kB). View file
|
|
common/exception/__pycache__/exception_handler.cpython-39.pyc
ADDED
Binary file (7.46 kB). View file
|
|
common/exception/errors.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
from typing import Any
|
4 |
+
|
5 |
+
from fastapi import HTTPException
|
6 |
+
from starlette.background import BackgroundTask
|
7 |
+
|
8 |
+
from common.response.response_code import CustomErrorCode, StandardResponseCode
|
9 |
+
|
10 |
+
|
11 |
+
class BaseExceptionMixin(Exception):
|
12 |
+
|
13 |
+
code: int
|
14 |
+
|
15 |
+
def __init__(self, *, msg: str = None, data: Any = None, background: BackgroundTask | None = None):
|
16 |
+
self.msg = msg
|
17 |
+
self.data = data
|
18 |
+
# The original background task: https://www.starlette.io/background/
|
19 |
+
self.background = background
|
20 |
+
|
21 |
+
|
22 |
+
class HTTPError(HTTPException):
|
23 |
+
"""HTTP """
|
24 |
+
|
25 |
+
def __init__(self, *, code: int, msg: Any = None, headers: dict[str, Any] | None = None):
|
26 |
+
super().__init__(status_code=code, detail=msg, headers=headers)
|
27 |
+
|
28 |
+
|
29 |
+
class CustomError(BaseExceptionMixin):
|
30 |
+
|
31 |
+
def __init__(self, *, error: CustomErrorCode, data: Any = None, background: BackgroundTask | None = None):
|
32 |
+
self.code = error.code
|
33 |
+
super().__init__(msg=error.msg, data=data, background=background)
|
34 |
+
|
35 |
+
|
36 |
+
class RequestError(BaseExceptionMixin):
|
37 |
+
code = StandardResponseCode.HTTP_400
|
38 |
+
|
39 |
+
def __init__(self, *, msg: str = 'Bad Request', data: Any = None, background: BackgroundTask | None = None):
|
40 |
+
super().__init__(msg=msg, data=data, background=background)
|
41 |
+
|
42 |
+
|
43 |
+
class ForbiddenError(BaseExceptionMixin):
|
44 |
+
|
45 |
+
code = StandardResponseCode.HTTP_403
|
46 |
+
|
47 |
+
def __init__(self, *, msg: str = 'Forbidden', data: Any = None, background: BackgroundTask | None = None):
|
48 |
+
super().__init__(msg=msg, data=data, background=background)
|
49 |
+
|
50 |
+
|
51 |
+
class NotFoundError(BaseExceptionMixin):
|
52 |
+
code = StandardResponseCode.HTTP_404
|
53 |
+
|
54 |
+
def __init__(self, *, msg: str = 'Not Found', data: Any = None, background: BackgroundTask | None = None):
|
55 |
+
super().__init__(msg=msg, data=data, background=background)
|
56 |
+
|
57 |
+
|
58 |
+
class ServerError(BaseExceptionMixin):
|
59 |
+
|
60 |
+
code = StandardResponseCode.HTTP_500
|
61 |
+
|
62 |
+
def __init__(
|
63 |
+
self, *, msg: str = 'Internal Server Error', data: Any = None, background: BackgroundTask | None = None
|
64 |
+
):
|
65 |
+
super().__init__(msg=msg, data=data, background=background)
|
66 |
+
|
67 |
+
|
68 |
+
class GatewayError(BaseExceptionMixin):
|
69 |
+
|
70 |
+
code = StandardResponseCode.HTTP_502
|
71 |
+
|
72 |
+
def __init__(self, *, msg: str = 'Bad Gateway', data: Any = None, background: BackgroundTask | None = None):
|
73 |
+
super().__init__(msg=msg, data=data, background=background)
|
74 |
+
|
75 |
+
|
76 |
+
class AuthorizationError(BaseExceptionMixin):
|
77 |
+
|
78 |
+
code = StandardResponseCode.HTTP_401
|
79 |
+
|
80 |
+
def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None):
|
81 |
+
super().__init__(msg=msg, data=data, background=background)
|
82 |
+
|
83 |
+
|
84 |
+
class TokenError(HTTPError):
|
85 |
+
|
86 |
+
code = StandardResponseCode.HTTP_401
|
87 |
+
|
88 |
+
def __init__(self, *, msg: str = 'Not Authenticated', headers: dict[str, Any] | None = None):
|
89 |
+
super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'})
|
common/exception/exception_handler.py
ADDED
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
from fastapi import FastAPI, Request
|
4 |
+
from fastapi.exceptions import RequestValidationError
|
5 |
+
from pydantic import ValidationError
|
6 |
+
from starlette.exceptions import HTTPException
|
7 |
+
from starlette.middleware.cors import CORSMiddleware
|
8 |
+
from uvicorn.protocols.http.h11_impl import STATUS_PHRASES
|
9 |
+
|
10 |
+
from common.exception.errors import BaseExceptionMixin
|
11 |
+
from common.response.response_code import CustomResponseCode, StandardResponseCode
|
12 |
+
from common.response.response_schema import response_base
|
13 |
+
from common.schema import (
|
14 |
+
CUSTOM_VALIDATION_ERROR_MESSAGES,
|
15 |
+
)
|
16 |
+
from core.conf import settings
|
17 |
+
from utils.serializers import MsgSpecJSONResponse
|
18 |
+
from utils.trace_id import get_request_trace_id
|
19 |
+
|
20 |
+
|
21 |
+
def _get_exception_code(status_code: int) -> int:
|
22 |
+
try:
|
23 |
+
STATUS_PHRASES[status_code]
|
24 |
+
return status_code
|
25 |
+
except Exception:
|
26 |
+
return StandardResponseCode.HTTP_400
|
27 |
+
|
28 |
+
|
29 |
+
async def _validation_exception_handler(request: Request, exc: RequestValidationError | ValidationError):
|
30 |
+
errors = []
|
31 |
+
for error in exc.errors():
|
32 |
+
custom_message = CUSTOM_VALIDATION_ERROR_MESSAGES.get(error['type'])
|
33 |
+
if custom_message:
|
34 |
+
ctx = error.get('ctx')
|
35 |
+
if not ctx:
|
36 |
+
error['msg'] = custom_message
|
37 |
+
else:
|
38 |
+
error['msg'] = custom_message.format(**ctx)
|
39 |
+
ctx_error = ctx.get('error')
|
40 |
+
if ctx_error:
|
41 |
+
error['ctx']['error'] = (
|
42 |
+
ctx_error.__str__().replace("'", '"') if isinstance(ctx_error, Exception) else None
|
43 |
+
)
|
44 |
+
errors.append(error)
|
45 |
+
error = errors[0]
|
46 |
+
if error.get('type') == 'json_invalid':
|
47 |
+
message = 'json解析失败'
|
48 |
+
else:
|
49 |
+
error_input = error.get('input')
|
50 |
+
field = str(error.get('loc')[-1])
|
51 |
+
error_msg = error.get('msg')
|
52 |
+
message = f'{field} {error_msg},输入:{error_input}' if settings.ENVIRONMENT == 'dev' else error_msg
|
53 |
+
msg = f'请求参数非法: {message}'
|
54 |
+
data = {'errors': errors} if settings.ENVIRONMENT == 'dev' else None
|
55 |
+
content = {
|
56 |
+
'code': StandardResponseCode.HTTP_422,
|
57 |
+
'msg': msg,
|
58 |
+
'data': data,
|
59 |
+
}
|
60 |
+
request.state.__request_validation_exception__ = content
|
61 |
+
content.update(trace_id=get_request_trace_id(request))
|
62 |
+
return MsgSpecJSONResponse(status_code=422, content=content)
|
63 |
+
|
64 |
+
|
65 |
+
def register_exception(app: FastAPI):
|
66 |
+
@app.exception_handler(HTTPException)
|
67 |
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
68 |
+
if settings.ENVIRONMENT == 'dev':
|
69 |
+
content = {
|
70 |
+
'code': exc.status_code,
|
71 |
+
'msg': exc.detail,
|
72 |
+
'data': None,
|
73 |
+
}
|
74 |
+
else:
|
75 |
+
res = response_base.fail(res=CustomResponseCode.HTTP_400)
|
76 |
+
content = res.model_dump()
|
77 |
+
request.state.__request_http_exception__ = content
|
78 |
+
content.update(trace_id=get_request_trace_id(request))
|
79 |
+
return MsgSpecJSONResponse(
|
80 |
+
status_code=_get_exception_code(exc.status_code),
|
81 |
+
content=content,
|
82 |
+
headers=exc.headers,
|
83 |
+
)
|
84 |
+
|
85 |
+
@app.exception_handler(RequestValidationError)
|
86 |
+
async def fastapi_validation_exception_handler(request: Request, exc: RequestValidationError):
|
87 |
+
return await _validation_exception_handler(request, exc)
|
88 |
+
|
89 |
+
@app.exception_handler(ValidationError)
|
90 |
+
async def pydantic_validation_exception_handler(request: Request, exc: ValidationError):
|
91 |
+
return await _validation_exception_handler(request, exc)
|
92 |
+
|
93 |
+
@app.exception_handler(AssertionError)
|
94 |
+
async def assertion_error_handler(request: Request, exc: AssertionError):
|
95 |
+
if settings.ENVIRONMENT == 'dev':
|
96 |
+
content = {
|
97 |
+
'code': StandardResponseCode.HTTP_500,
|
98 |
+
'msg': str(''.join(exc.args) if exc.args else exc.__doc__),
|
99 |
+
'data': None,
|
100 |
+
}
|
101 |
+
else:
|
102 |
+
res = response_base.fail(res=CustomResponseCode.HTTP_500)
|
103 |
+
content = res.model_dump()
|
104 |
+
request.state.__request_assertion_error__ = content
|
105 |
+
content.update(trace_id=get_request_trace_id(request))
|
106 |
+
return MsgSpecJSONResponse(
|
107 |
+
status_code=StandardResponseCode.HTTP_500,
|
108 |
+
content=content,
|
109 |
+
)
|
110 |
+
|
111 |
+
@app.exception_handler(BaseExceptionMixin)
|
112 |
+
async def custom_exception_handler(request: Request, exc: BaseExceptionMixin):
|
113 |
+
content = {
|
114 |
+
'code': exc.code,
|
115 |
+
'msg': str(exc.msg),
|
116 |
+
'data': exc.data if exc.data else None,
|
117 |
+
}
|
118 |
+
request.state.__request_custom_exception__ = content
|
119 |
+
content.update(trace_id=get_request_trace_id(request))
|
120 |
+
return MsgSpecJSONResponse(
|
121 |
+
status_code=_get_exception_code(exc.code),
|
122 |
+
content=content,
|
123 |
+
background=exc.background,
|
124 |
+
)
|
125 |
+
|
126 |
+
@app.exception_handler(Exception)
|
127 |
+
async def all_unknown_exception_handler(request: Request, exc: Exception):
|
128 |
+
if settings.ENVIRONMENT == 'dev':
|
129 |
+
content = {
|
130 |
+
'code': StandardResponseCode.HTTP_500,
|
131 |
+
'msg': str(exc),
|
132 |
+
'data': None,
|
133 |
+
}
|
134 |
+
else:
|
135 |
+
res = response_base.fail(res=CustomResponseCode.HTTP_500)
|
136 |
+
content = res.model_dump()
|
137 |
+
request.state.__request_all_unknown_exception__ = content
|
138 |
+
content.update(trace_id=get_request_trace_id(request))
|
139 |
+
return MsgSpecJSONResponse(
|
140 |
+
status_code=StandardResponseCode.HTTP_500,
|
141 |
+
content=content,
|
142 |
+
)
|
143 |
+
|
144 |
+
if settings.MIDDLEWARE_CORS:
|
145 |
+
|
146 |
+
@app.exception_handler(StandardResponseCode.HTTP_500)
|
147 |
+
async def cors_custom_code_500_exception_handler(request, exc):
|
148 |
+
"""
|
149 |
+
500
|
150 |
+
|
151 |
+
`Related issue <https://github.com/encode/starlette/issues/1175>`_
|
152 |
+
|
153 |
+
`Solution <https://github.com/fastapi/fastapi/discussions/7847#discussioncomment-5144709>`_
|
154 |
+
|
155 |
+
:param request: FastAPI
|
156 |
+
:param exc:
|
157 |
+
:return:
|
158 |
+
"""
|
159 |
+
if isinstance(exc, BaseExceptionMixin):
|
160 |
+
content = {
|
161 |
+
'code': exc.code,
|
162 |
+
'msg': exc.msg,
|
163 |
+
'data': exc.data,
|
164 |
+
}
|
165 |
+
else:
|
166 |
+
if settings.ENVIRONMENT == 'dev':
|
167 |
+
content = {
|
168 |
+
'code': StandardResponseCode.HTTP_500,
|
169 |
+
'msg': str(exc),
|
170 |
+
'data': None,
|
171 |
+
}
|
172 |
+
else:
|
173 |
+
res = response_base.fail(res=CustomResponseCode.HTTP_500)
|
174 |
+
content = res.model_dump()
|
175 |
+
request.state.__request_cors_500_exception__ = content
|
176 |
+
content.update(trace_id=get_request_trace_id(request))
|
177 |
+
response = MsgSpecJSONResponse(
|
178 |
+
status_code=exc.code if isinstance(exc, BaseExceptionMixin) else StandardResponseCode.HTTP_500,
|
179 |
+
content=content,
|
180 |
+
background=exc.background if isinstance(exc, BaseExceptionMixin) else None,
|
181 |
+
)
|
182 |
+
origin = request.headers.get('origin')
|
183 |
+
if origin:
|
184 |
+
cors = CORSMiddleware(
|
185 |
+
app=app,
|
186 |
+
allow_origins=settings.CORS_ALLOWED_ORIGINS,
|
187 |
+
allow_credentials=True,
|
188 |
+
allow_methods=['*'],
|
189 |
+
allow_headers=['*'],
|
190 |
+
expose_headers=settings.CORS_EXPOSE_HEADERS,
|
191 |
+
)
|
192 |
+
response.headers.update(cors.simple_headers)
|
193 |
+
has_cookie = 'cookie' in request.headers
|
194 |
+
if cors.allow_all_origins and has_cookie:
|
195 |
+
response.headers['Access-Control-Allow-Origin'] = origin
|
196 |
+
elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin):
|
197 |
+
response.headers['Access-Control-Allow-Origin'] = origin
|
198 |
+
response.headers.add_vary_header('Origin')
|
199 |
+
return response
|
common/external/__pycache__/external_api.cpython-310.pyc
ADDED
Binary file (3.35 kB). View file
|
|
common/external/external_api.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import httpx
|
2 |
+
from core.conf import settings
|
3 |
+
from typing import Optional, Dict, Any
|
4 |
+
from core.token_manager import get_access_token
|
5 |
+
|
6 |
+
API_BASE_URL = settings.API_BASE_URL
|
7 |
+
|
8 |
+
class API():
|
9 |
+
@staticmethod
|
10 |
+
async def get(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, api_base: str = None):
|
11 |
+
if api_base:
|
12 |
+
url = f"{api_base}{endpoint}"
|
13 |
+
else:
|
14 |
+
url = f"{API_BASE_URL}{endpoint}"
|
15 |
+
access_token = get_access_token()
|
16 |
+
headers = headers or {}
|
17 |
+
headers.setdefault("Content-Type", "application/json")
|
18 |
+
headers["Authorization"] = f"Bearer {access_token}"
|
19 |
+
async with httpx.AsyncClient() as client:
|
20 |
+
try:
|
21 |
+
response = await client.get(url, headers=headers, params=params)
|
22 |
+
response.raise_for_status()
|
23 |
+
return response.json()
|
24 |
+
except httpx.HTTPStatusError as http_err:
|
25 |
+
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
26 |
+
except Exception as err:
|
27 |
+
return {"error": f"Request failed: {str(err)}"}
|
28 |
+
|
29 |
+
@staticmethod
|
30 |
+
async def post(endpoint: str , payload: Dict[str, Any] = None,headers: Optional[Dict[str, str]] = None, api_base: str = None):
|
31 |
+
if api_base:
|
32 |
+
url = f"{api_base}{endpoint}"
|
33 |
+
else:
|
34 |
+
url = f"{API_BASE_URL}{endpoint}"
|
35 |
+
access_token = get_access_token()
|
36 |
+
headers = headers or {}
|
37 |
+
headers.setdefault("Content-Type", "application/json")
|
38 |
+
headers["Authorization"] = f"Bearer {access_token}"
|
39 |
+
async with httpx.AsyncClient() as client:
|
40 |
+
try:
|
41 |
+
response = await client.post(url, json=payload, headers=headers)
|
42 |
+
response.raise_for_status()
|
43 |
+
return response.json()
|
44 |
+
except httpx.HTTPStatusError as http_err:
|
45 |
+
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
46 |
+
except Exception as err:
|
47 |
+
return {"error": f"Request failed: {str(err)}"}
|
48 |
+
|
49 |
+
@staticmethod
|
50 |
+
async def put_api(endpoint: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None):
|
51 |
+
url = f"{API_BASE_URL}{endpoint}"
|
52 |
+
access_token = get_access_token()
|
53 |
+
headers = headers or {}
|
54 |
+
headers.setdefault("Content-Type", "application/json")
|
55 |
+
headers["Authorization"] = f"Bearer {access_token}"
|
56 |
+
async with httpx.AsyncClient() as client:
|
57 |
+
try:
|
58 |
+
response = await client.put(url, json=payload, headers=headers)
|
59 |
+
response.raise_for_status()
|
60 |
+
return response.json()
|
61 |
+
except httpx.HTTPStatusError as http_err:
|
62 |
+
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
63 |
+
except Exception as err:
|
64 |
+
return {"error": f"Request failed: {str(err)}"}
|
65 |
+
|
66 |
+
@staticmethod
|
67 |
+
async def delete_api(endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None):
|
68 |
+
url = f"{API_BASE_URL}{endpoint}"
|
69 |
+
access_token = get_access_token()
|
70 |
+
headers = headers or {}
|
71 |
+
headers.setdefault("Content-Type", "application/json")
|
72 |
+
headers["Authorization"] = f"Bearer {access_token}"
|
73 |
+
async with httpx.AsyncClient() as client:
|
74 |
+
try:
|
75 |
+
response = await client.delete(url, headers=headers, params=params)
|
76 |
+
response.raise_for_status()
|
77 |
+
return response.json()
|
78 |
+
except httpx.HTTPStatusError as http_err:
|
79 |
+
return {"error": f"HTTP {http_err.response.status_code}: {http_err.response.text}"}
|
80 |
+
except Exception as err:
|
81 |
+
return {"error": f"Request failed: {str(err)}"}
|
82 |
+
|
83 |
+
api: API = API()
|
common/log.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
import inspect
|
4 |
+
import logging
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
|
8 |
+
from asgi_correlation_id import correlation_id
|
9 |
+
from loguru import logger
|
10 |
+
|
11 |
+
from core import path_conf
|
12 |
+
from core.conf import settings
|
13 |
+
|
14 |
+
|
15 |
+
class InterceptHandler(logging.Handler):
|
16 |
+
def emit(self, record: logging.LogRecord):
|
17 |
+
# 获取对应的 Loguru 级别(如果存在)
|
18 |
+
try:
|
19 |
+
level = logger.level(record.levelname).name
|
20 |
+
except ValueError:
|
21 |
+
level = record.levelno
|
22 |
+
|
23 |
+
# 查找记录日志消息的调用者
|
24 |
+
frame, depth = inspect.currentframe(), 0
|
25 |
+
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
|
26 |
+
frame = frame.f_back
|
27 |
+
depth += 1
|
28 |
+
|
29 |
+
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
30 |
+
|
31 |
+
|
32 |
+
def setup_logging() -> None:
|
33 |
+
"""
|
34 |
+
- https://github.com/benoitc/gunicorn/issues/1572#issuecomment-638391953
|
35 |
+
- https://github.com/pawamoy/pawamoy.github.io/issues/17
|
36 |
+
"""
|
37 |
+
logging.root.handlers = [InterceptHandler()]
|
38 |
+
logging.root.setLevel(settings.LOG_STD_LEVEL)
|
39 |
+
|
40 |
+
for name in logging.root.manager.loggerDict.keys():
|
41 |
+
logging.getLogger(name).handlers = []
|
42 |
+
if 'uvicorn.access' in name or 'watchfiles.main' in name:
|
43 |
+
logging.getLogger(name).propagate = False
|
44 |
+
else:
|
45 |
+
logging.getLogger(name).propagate = True
|
46 |
+
|
47 |
+
# Debug log handlers
|
48 |
+
# logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}')
|
49 |
+
|
50 |
+
# https://github.com/snok/asgi-correlation-id/issues/7
|
51 |
+
def correlation_id_filter(record):
|
52 |
+
cid = correlation_id.get(settings.LOG_CID_DEFAULT_VALUE)
|
53 |
+
record['correlation_id'] = cid[: settings.LOG_CID_UUID_LENGTH]
|
54 |
+
return record
|
55 |
+
|
56 |
+
# 配置 loguru 处理器
|
57 |
+
logger.remove() # 移除默认处理器
|
58 |
+
logger.configure(
|
59 |
+
handlers=[
|
60 |
+
{
|
61 |
+
'sink': sys.stdout,
|
62 |
+
'level': settings.LOG_STD_LEVEL,
|
63 |
+
'filter': lambda record: correlation_id_filter(record),
|
64 |
+
'format': settings.LOG_STD_FORMAT,
|
65 |
+
}
|
66 |
+
]
|
67 |
+
)
|
68 |
+
|
69 |
+
|
70 |
+
def set_custom_logfile() -> None:
|
71 |
+
log_path = path_conf.LOG_DIR
|
72 |
+
if not os.path.exists(log_path):
|
73 |
+
os.mkdir(log_path)
|
74 |
+
|
75 |
+
log_access_file = os.path.join(log_path, settings.LOG_ACCESS_FILENAME)
|
76 |
+
log_error_file = os.path.join(log_path, settings.LOG_ERROR_FILENAME)
|
77 |
+
|
78 |
+
# https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add
|
79 |
+
log_config = {
|
80 |
+
'format': settings.LOG_FILE_FORMAT,
|
81 |
+
'enqueue': True,
|
82 |
+
'rotation': '5 MB',
|
83 |
+
'retention': '7 days',
|
84 |
+
'compression': 'tar.gz',
|
85 |
+
}
|
86 |
+
|
87 |
+
logger.add(
|
88 |
+
str(log_access_file),
|
89 |
+
level=settings.LOG_ACCESS_FILE_LEVEL,
|
90 |
+
filter=lambda record: record['level'].no <= 25,
|
91 |
+
backtrace=False,
|
92 |
+
diagnose=False,
|
93 |
+
**log_config,
|
94 |
+
)
|
95 |
+
|
96 |
+
logger.add(
|
97 |
+
str(log_error_file),
|
98 |
+
level=settings.LOG_ERROR_FILE_LEVEL,
|
99 |
+
filter=lambda record: record['level'].no >= 30,
|
100 |
+
backtrace=True,
|
101 |
+
diagnose=True,
|
102 |
+
**log_config,
|
103 |
+
)
|
104 |
+
|
105 |
+
|
106 |
+
log = logger
|