Utoljára aktív 9 months ago

提供一個整合資料庫連線、API 金鑰驗證與基本路由的 Flask 應用程式範本,適合作為 Web API 開發起點。

timmy gist felülvizsgálása 9 months ago. Revízióhoz ugrás

1 file changed, 68 insertions

README.md(fájl létrehozva)

@@ -0,0 +1,68 @@
1 + # 專案簡介
2 +
3 + 本專案展示如何整合環境變數管理、日誌系統、資料庫連線封裝與 API 設計,並透過 Flask 框架提供 RESTful 服務。專案強調資源管理與安全驗證的流程,適合用於建立穩定且安全的後端服務。
4 +
5 + ---
6 +
7 + ## 初始化與環境設定
8 +
9 + 1. **環境變數讀取**
10 + 程式啟動時會使用 `dotenv` 載入 .env 檔案中的設定,包含 API 金鑰、資料庫連線參數、日誌等級及密鑰等。
11 +
12 + 2. **日誌工具設定**
13 + 利用 loguru 設定日誌輸出,記錄連線狀態與錯誤資訊,方便追蹤與除錯。
14 +
15 + ---
16 +
17 + ## 資料庫連線模組
18 +
19 + 1. **BaseDatabase 類別**
20 + 封裝與資料庫互動的基本流程,包含建立連線、執行 SQL 查詢、將查詢結果轉換為 DataFrame 以及正確關閉連線。
21 +
22 + 2. **子類別擴展**
23 + 根據不同資料庫(如 SQL Server、SQLite、MySQL),定義各自的子類別,依資料庫連線格式產生合適的連線 URL,並調用 BaseDatabase 功能。
24 +
25 + ---
26 +
27 + ## 資料庫連線管理
28 +
29 + - **get_db 函式**
30 + 從環境變數中讀取 SQL Server 的連線參數,建立資料庫連線實例,確保每次請求都能根據設定取得正確的連線。
31 +
32 + ---
33 +
34 + ## Flask 應用程式建立與配置
35 +
36 + 1. **建立 Flask 應用**
37 + 透過 `create_app()` 函式建立並配置 Flask 應用,並設定必要參數(例如 SECRET_KEY)。
38 +
39 + 2. **生命週期鉤子**
40 + - **before_request**:每個 HTTP 請求前,請透過 `get_db()` 初始化資料庫連線,並將連線存放於全域變數 `g` 中。
41 + - **teardown_request**:請求結束後,不論是否發生錯誤,都會關閉資料庫連線,以確保資源正確釋放。
42 +
43 + ---
44 +
45 + ## API 路由與邏輯處理
46 +
47 + 1. **API 金鑰驗證**
48 + 請在進入 API 邏輯前,透過 `validate_api_key()` 驗證請求是否包含正確的 API 金鑰,以保護 API 存取權限。
49 +
50 + 2. **資料查詢 API (`/data`)**
51 +
52 + - 接收 `start` 與 `end` 兩個時間參數,並驗證其格式是否正確。
53 + - 根據提供的時間區間構造 SQL 查詢,從資料表中抓取符合條件的資料。
54 + - 查詢結果將轉換為 Pandas DataFrame,最終以 JSON 格式回傳給請求端。
55 +
56 + 3. **健康檢查 API (`/health`)**
57 + 提供一個簡單的端點來回報服務狀態,常用於監控或自動化健康檢查。
58 +
59 + ---
60 +
61 + ## 主程式執行
62 +
63 + - **使用 Waitress 啟動服務**
64 + 主程式區塊中將建立好的 Flask 應用交由 Waitress 伺服器運行,使服務能在指定主機與埠口上穩定提供 HTTP 服務。
65 +
66 + ---
67 +
68 + 整體而言,此專案展示了如何整合多項技術與流程,達成穩定、安全的後端服務。請依據實際需求進行修改與擴充,如有任何問題,歡迎提出討論。

timmy gist felülvizsgálása 9 months ago. Revízióhoz ugrás

2 files changed, 221 insertions

template.py(fájl létrehozva)

@@ -0,0 +1,198 @@
1 + #!/usr/bin/env python
2 +
3 + import os
4 + import sys
5 + from urllib.parse import quote_plus
6 + from datetime import datetime
7 +
8 + from dotenv import load_dotenv
9 + from flask import Flask, request, jsonify, g
10 + import records
11 + import pandas as pd
12 + from loguru import logger
13 +
14 + # 載入 .env 檔案中的環境變數
15 + load_dotenv()
16 +
17 + # 應用程式與 API 設定參數
18 + SECRET_KEY = os.getenv("SECRET_KEY", "default_secret")
19 + API_KEY = os.getenv("API_KEY", "your_generic_api_key_here")
20 + LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
21 +
22 + # 設定 loguru 日誌工具,將日誌輸出到標準輸出
23 + logger.remove()
24 + logger.add(
25 + sys.stdout,
26 + level=LOG_LEVEL,
27 + format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level}</level> | <cyan>{message}</cyan>",
28 + )
29 +
30 + # ------------------ 資料庫連線模組 ------------------
31 +
32 + class BaseDatabase:
33 + def __init__(self, url):
34 + self.url = url
35 + logger.debug(f"資料庫 URL: {self.url}")
36 + try:
37 + self.db = records.Database(self.url)
38 + logger.info("資料庫連線初始化成功。")
39 + except Exception as e:
40 + logger.error(f"資料庫連線初始化失敗: {e}")
41 + raise
42 +
43 + def query(self, sql):
44 + try:
45 + logger.debug(f"執行 SQL 查詢: {sql}")
46 + self.result = self.db.query(sql, fetchall=True)
47 + logger.info("查詢執行成功。")
48 + except Exception as e:
49 + logger.error(f"查詢執行發生錯誤: {e}")
50 + self.result = None
51 +
52 + def to_dataframe(self):
53 + if self.result:
54 + try:
55 + data = self.result.dataset.dict
56 + logger.debug(f"查詢結果轉換為 DataFrame: {data}")
57 + return pd.DataFrame.from_dict(data)
58 + except Exception as e:
59 + logger.error(f"轉換為 DataFrame 失敗: {e}")
60 + return pd.DataFrame()
61 + else:
62 + logger.warning("查詢結果為空。")
63 + return pd.DataFrame()
64 +
65 + def close(self):
66 + try:
67 + self.db.close()
68 + logger.info("資料庫連線已成功關閉。")
69 + except Exception as e:
70 + logger.error(f"關閉資料庫連線失敗: {e}")
71 +
72 + def test_connection(self):
73 + try:
74 + self.query("SELECT 1+1 AS result")
75 + if self.result:
76 + for row in self.result:
77 + row_dict = row.as_dict()
78 + return row_dict.get("result")
79 + else:
80 + logger.warning("測試連線查詢無回傳結果。")
81 + return None
82 + except Exception as e:
83 + logger.error(f"測試連線時發生錯誤: {e}")
84 + return None
85 +
86 + class SqlSrv(BaseDatabase):
87 + def __init__(self, server, username, password, db_name):
88 + if None in [server, username, password, db_name]:
89 + raise ValueError("資料庫連線參數不足。")
90 + password_encoded = quote_plus(password)
91 + url = f"mssql+pymssql://{username}:{password_encoded}@{server}:1433/{db_name}?tds_version=7.0"
92 + super().__init__(url)
93 +
94 + class SQLite(BaseDatabase):
95 + def __init__(self, db_path):
96 + url = f"sqlite:///{db_path}"
97 + super().__init__(url)
98 +
99 + class MySQL(BaseDatabase):
100 + def __init__(self, server, username, password, db_name):
101 + url = f"mysql+pymysql://{username}:{quote_plus(password)}@{server}/{db_name}"
102 + super().__init__(url)
103 +
104 + def get_db():
105 + """
106 + 從環境變數中讀取資料庫連線參數,
107 + 建立 SQL Server 連線(可根據需求改用 SQLite 或 MySQL)。
108 + """
109 + DB_SERVER = os.getenv("DB_SERVER")
110 + DB_USERNAME = os.getenv("DB_USERNAME")
111 + DB_PASSWORD = os.getenv("DB_PASSWORD")
112 + DB_NAME = os.getenv("DB_NAME")
113 + return SqlSrv(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME)
114 +
115 + # ------------------ API 工具與路由設定 ------------------
116 +
117 + def validate_api_key():
118 + """
119 + 檢查請求中的 API 金鑰是否正確。
120 + """
121 + key = request.headers.get("Authorization") or request.headers.get("X-API-KEY")
122 + if not key or (key != f"Bearer {API_KEY}" and key != API_KEY):
123 + return False
124 + return True
125 +
126 + def create_app():
127 + """
128 + 建立並配置 Flask 應用程式,
129 + 包含資料庫連線初始化、路由註冊等設定。
130 + """
131 + app = Flask(__name__)
132 + app.config['SECRET_KEY'] = SECRET_KEY
133 +
134 + @app.before_request
135 + def before_request():
136 + """為每個請求初始化資料庫連線。"""
137 + g.db = get_db()
138 +
139 + @app.teardown_request
140 + def teardown_request(exception=None):
141 + """在請求結束時關閉資料庫連線。"""
142 + db = getattr(g, 'db', None)
143 + if db is not None:
144 + db.close()
145 +
146 + @app.route('/data', methods=['GET'])
147 + def get_data():
148 + """
149 + 資料查詢 API:
150 + - 驗證 API 金鑰
151 + - 解析請求參數(start 與 end)
152 + - 執行 SQL 查詢並回傳 JSON 格式的查詢結果
153 + """
154 + if not validate_api_key():
155 + return jsonify({"error": "Unauthorized"}), 401
156 +
157 + start_time = request.args.get('start')
158 + end_time = request.args.get('end')
159 +
160 + if not start_time or not end_time:
161 + return jsonify({"error": "Please provide 'start' and 'end' parameters"}), 400
162 +
163 + try:
164 + start_dt = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
165 + end_dt = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
166 + except ValueError:
167 + return jsonify({"error": "Invalid date format. Use YYYY-MM-DD HH:MM:SS"}), 400
168 +
169 + # 示範查詢語法,請根據實際需求修改資料表名稱與欄位
170 + query = f"""
171 + SELECT *
172 + FROM my_table
173 + WHERE created_at >= '{start_dt}'
174 + AND created_at <= '{end_dt}'
175 + ORDER BY created_at ASC
176 + """
177 + g.db.query(query)
178 + df = g.db.to_dataframe()
179 +
180 + if df.empty:
181 + return jsonify([])
182 +
183 + return jsonify(df.to_dict(orient="records"))
184 +
185 + @app.route('/health', methods=['GET'])
186 + def health_check():
187 + """健康檢查 API,回傳伺服器狀態。"""
188 + return jsonify({"status": "ok"})
189 +
190 + return app
191 +
192 + # ------------------ 主程式執行進入點 ------------------
193 +
194 + if __name__ == '__main__':
195 + from waitress import serve
196 + app = create_app()
197 + print("Starting generic server with Waitress...")
198 + serve(app, host='0.0.0.0', port=5000)

test_api.sh(fájl létrehozva)

@@ -0,0 +1,23 @@
1 + #!/bin/bash
2 +
3 + # 測試 API /data 端點的 curl 測試 script
4 + # Usage: ./test_api.sh '2025-01-01 00:00:00' '2025-01-02 00:00:00'
5 +
6 + API_URL="http://127.0.0.1:5000/data"
7 + API_KEY="your_generic_api_key_here" # 請替換成正確的 API 金鑰
8 +
9 + if [ "$#" -lt 2 ]; then
10 + echo "Usage: $0 <start_time> <end_time>"
11 + echo "Example: $0 '2025-01-01 00:00:00' '2025-01-02 00:00:00'"
12 + exit 1
13 + fi
14 +
15 + START_TIME="$1"
16 + END_TIME="$2"
17 +
18 + # 將時間中的空格轉換成 URL 可讀格式
19 + ENCODED_START=$(echo "$START_TIME" | sed 's/ /%20/g')
20 + ENCODED_END=$(echo "$END_TIME" | sed 's/ /%20/g')
21 +
22 + curl -H "Authorization: Bearer $API_KEY" \
23 + "$API_URL?start=$ENCODED_START&end=$ENCODED_END"
Újabb Régebbi