template.py
· 6.3 KiB · Python
原始檔案
#!/usr/bin/env python
import os
import sys
from urllib.parse import quote_plus
from datetime import datetime
from dotenv import load_dotenv
from flask import Flask, request, jsonify, g
import records
import pandas as pd
from loguru import logger
# 載入 .env 檔案中的環境變數
load_dotenv()
# 應用程式與 API 設定參數
SECRET_KEY = os.getenv("SECRET_KEY", "default_secret")
API_KEY = os.getenv("API_KEY", "your_generic_api_key_here")
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
# 設定 loguru 日誌工具,將日誌輸出到標準輸出
logger.remove()
logger.add(
sys.stdout,
level=LOG_LEVEL,
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level}</level> | <cyan>{message}</cyan>",
)
# ------------------ 資料庫連線模組 ------------------
class BaseDatabase:
def __init__(self, url):
self.url = url
logger.debug(f"資料庫 URL: {self.url}")
try:
self.db = records.Database(self.url)
logger.info("資料庫連線初始化成功。")
except Exception as e:
logger.error(f"資料庫連線初始化失敗: {e}")
raise
def query(self, sql):
try:
logger.debug(f"執行 SQL 查詢: {sql}")
self.result = self.db.query(sql, fetchall=True)
logger.info("查詢執行成功。")
except Exception as e:
logger.error(f"查詢執行發生錯誤: {e}")
self.result = None
def to_dataframe(self):
if self.result:
try:
data = self.result.dataset.dict
logger.debug(f"查詢結果轉換為 DataFrame: {data}")
return pd.DataFrame.from_dict(data)
except Exception as e:
logger.error(f"轉換為 DataFrame 失敗: {e}")
return pd.DataFrame()
else:
logger.warning("查詢結果為空。")
return pd.DataFrame()
def close(self):
try:
self.db.close()
logger.info("資料庫連線已成功關閉。")
except Exception as e:
logger.error(f"關閉資料庫連線失敗: {e}")
def test_connection(self):
try:
self.query("SELECT 1+1 AS result")
if self.result:
for row in self.result:
row_dict = row.as_dict()
return row_dict.get("result")
else:
logger.warning("測試連線查詢無回傳結果。")
return None
except Exception as e:
logger.error(f"測試連線時發生錯誤: {e}")
return None
class SqlSrv(BaseDatabase):
def __init__(self, server, username, password, db_name):
if None in [server, username, password, db_name]:
raise ValueError("資料庫連線參數不足。")
password_encoded = quote_plus(password)
url = f"mssql+pymssql://{username}:{password_encoded}@{server}:1433/{db_name}?tds_version=7.0"
super().__init__(url)
class SQLite(BaseDatabase):
def __init__(self, db_path):
url = f"sqlite:///{db_path}"
super().__init__(url)
class MySQL(BaseDatabase):
def __init__(self, server, username, password, db_name):
url = f"mysql+pymysql://{username}:{quote_plus(password)}@{server}/{db_name}"
super().__init__(url)
def get_db():
"""
從環境變數中讀取資料庫連線參數,
建立 SQL Server 連線(可根據需求改用 SQLite 或 MySQL)。
"""
DB_SERVER = os.getenv("DB_SERVER")
DB_USERNAME = os.getenv("DB_USERNAME")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_NAME = os.getenv("DB_NAME")
return SqlSrv(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME)
# ------------------ API 工具與路由設定 ------------------
def validate_api_key():
"""
檢查請求中的 API 金鑰是否正確。
"""
key = request.headers.get("Authorization") or request.headers.get("X-API-KEY")
if not key or (key != f"Bearer {API_KEY}" and key != API_KEY):
return False
return True
def create_app():
"""
建立並配置 Flask 應用程式,
包含資料庫連線初始化、路由註冊等設定。
"""
app = Flask(__name__)
app.config['SECRET_KEY'] = SECRET_KEY
@app.before_request
def before_request():
"""為每個請求初始化資料庫連線。"""
g.db = get_db()
@app.teardown_request
def teardown_request(exception=None):
"""在請求結束時關閉資料庫連線。"""
db = getattr(g, 'db', None)
if db is not None:
db.close()
@app.route('/data', methods=['GET'])
def get_data():
"""
資料查詢 API:
- 驗證 API 金鑰
- 解析請求參數(start 與 end)
- 執行 SQL 查詢並回傳 JSON 格式的查詢結果
"""
if not validate_api_key():
return jsonify({"error": "Unauthorized"}), 401
start_time = request.args.get('start')
end_time = request.args.get('end')
if not start_time or not end_time:
return jsonify({"error": "Please provide 'start' and 'end' parameters"}), 400
try:
start_dt = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
end_dt = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD HH:MM:SS"}), 400
# 示範查詢語法,請根據實際需求修改資料表名稱與欄位
query = f"""
SELECT *
FROM my_table
WHERE created_at >= '{start_dt}'
AND created_at <= '{end_dt}'
ORDER BY created_at ASC
"""
g.db.query(query)
df = g.db.to_dataframe()
if df.empty:
return jsonify([])
return jsonify(df.to_dict(orient="records"))
@app.route('/health', methods=['GET'])
def health_check():
"""健康檢查 API,回傳伺服器狀態。"""
return jsonify({"status": "ok"})
return app
# ------------------ 主程式執行進入點 ------------------
if __name__ == '__main__':
from waitress import serve
app = create_app()
print("Starting generic server with Waitress...")
serve(app, host='0.0.0.0', port=5000)
| 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) |
| 199 |
test_api.sh
· 667 B · Bash
原始檔案
#!/bin/bash
# 測試 API /data 端點的 curl 測試 script
# Usage: ./test_api.sh '2025-01-01 00:00:00' '2025-01-02 00:00:00'
API_URL="http://127.0.0.1:5000/data"
API_KEY="your_generic_api_key_here" # 請替換成正確的 API 金鑰
if [ "$#" -lt 2 ]; then
echo "Usage: $0 <start_time> <end_time>"
echo "Example: $0 '2025-01-01 00:00:00' '2025-01-02 00:00:00'"
exit 1
fi
START_TIME="$1"
END_TIME="$2"
# 將時間中的空格轉換成 URL 可讀格式
ENCODED_START=$(echo "$START_TIME" | sed 's/ /%20/g')
ENCODED_END=$(echo "$END_TIME" | sed 's/ /%20/g')
curl -H "Authorization: Bearer $API_KEY" \
"$API_URL?start=$ENCODED_START&end=$ENCODED_END"
| 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" |
| 24 |