OnlyBiggg commited on
Commit
5564ecb
·
1 Parent(s): 456b94f

'refactor'

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +0 -1
  2. .gitignore +1 -0
  3. Dockerfile +4 -2
  4. __pycache__/app.cpython-39.pyc +0 -0
  5. __pycache__/main.cpython-39.pyc +0 -0
  6. app/__init__.py +0 -0
  7. app/__pycache__/__init__.cpython-310.pyc +0 -0
  8. app/__pycache__/router.cpython-310.pyc +0 -0
  9. app/busbooking-451909-aa92e337868d.json +0 -13
  10. app/core/config.py +0 -9
  11. app/dialogflow/api/__init__.py +0 -0
  12. app/dialogflow/api/__pycache__/__init__.cpython-310.pyc +0 -0
  13. app/dialogflow/api/__pycache__/router.cpython-310.pyc +0 -0
  14. app/{api → dialogflow/api}/__pycache__/routes.cpython-39.pyc +0 -0
  15. app/dialogflow/api/router.py +10 -0
  16. app/dialogflow/api/v1/__init__.py +0 -0
  17. app/dialogflow/api/v1/__pycache__/__init__.cpython-310.pyc +0 -0
  18. app/dialogflow/api/v1/__pycache__/dialogflow.cpython-310.pyc +0 -0
  19. app/{api/routes.py → dialogflow/api/v1/dialogflow.py} +68 -62
  20. app/dialogflow/schemas/__init__.py +0 -0
  21. app/dialogflow/services/__init__.py +0 -0
  22. app/dialogflow/services/__pycache__/__init__.cpython-310.pyc +0 -0
  23. app/{services → dialogflow/services}/__pycache__/api.cpython-39.pyc +0 -0
  24. app/dialogflow/services/__pycache__/dialog_service.cpython-310.pyc +0 -0
  25. app/{services → dialogflow/services}/__pycache__/external_api.cpython-39.pyc +0 -0
  26. app/dialogflow/services/__pycache__/origin_codes.cpython-310.pyc +0 -0
  27. app/dialogflow/services/dialog_service.py +57 -0
  28. app/dialogflow/services/origin_codes.py +4 -0
  29. app/router.py +9 -0
  30. app/services/api.py +0 -61
  31. app/types/__pycache__/Respone.cpython-39.pyc +0 -0
  32. app/utils/__pycache__/constants.cpython-39.pyc +0 -0
  33. app/utils/constants.py +0 -5
  34. common/__init__.py +0 -0
  35. common/__pycache__/__init__.cpython-310.pyc +0 -0
  36. common/__pycache__/__init__.cpython-39.pyc +0 -0
  37. common/__pycache__/log.cpython-310.pyc +0 -0
  38. common/__pycache__/schema.cpython-310.pyc +0 -0
  39. common/exception/__init__.py +2 -0
  40. common/exception/__pycache__/__init__.cpython-310.pyc +0 -0
  41. common/exception/__pycache__/__init__.cpython-39.pyc +0 -0
  42. common/exception/__pycache__/errors.cpython-310.pyc +0 -0
  43. common/exception/__pycache__/errors.cpython-39.pyc +0 -0
  44. common/exception/__pycache__/exception_handler.cpython-310.pyc +0 -0
  45. common/exception/__pycache__/exception_handler.cpython-39.pyc +0 -0
  46. common/exception/errors.py +89 -0
  47. common/exception/exception_handler.py +199 -0
  48. common/external/__pycache__/external_api.cpython-310.pyc +0 -0
  49. common/external/external_api.py +83 -0
  50. 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.9
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 app.services import api
9
- from app.utils.constants import code_province
10
- from app.types.Respone import DialogFlowResponseAPI
11
  router = APIRouter()
12
 
13
- templates = Jinja2Templates(directory="app/templates")
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 = code_province.get(raw_departure_city)
64
- destination_code = code_province.get(raw_destination_city)
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 = code_province.get(raw_departure_city)
138
- destination_code = code_province.get(raw_destination_city)
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 = code_province.get(raw_departure_city)
203
- destination_code = code_province.get(raw_destination_city)
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 xe {route_name}\n" + " | ".join(map(str, time_list))]
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