From 099a79fe5f6554e0f1e1223c24be2d3780a53f19 Mon Sep 17 00:00:00 2001 From: Murasame Noa <162931251+LeiSureLyYrsc@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:29:23 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E6=88=90=20FastAPI=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 重写成FastAPI * 修改依赖相关配置 - pyproject `uvicorn[standard] -> fastapi[standard]` - requirements.txt 使用 `uv export` 生成 - Dockerfile 换用 `python:3.13` - README pip 安装增加版本提示 --------- Co-authored-by: wyf9 --- .vscode/settings.json | 4 +++ Dockerfile | 4 +-- README.md | 6 +++-- app.py | 55 ++++++++++++++++++++++++------------------ pyproject.toml | 6 ++--- requirements.txt | Bin 112 -> 20773 bytes 6 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ba2a6c0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c313d21..8f7a959 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.13 WORKDIR /app @@ -6,4 +6,4 @@ COPY . . RUN pip install gunicorn && pip install -r requirements.txt EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"] \ No newline at end of file +CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"] diff --git a/README.md b/README.md index b833625..0208e49 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # McStatus-API -这是一个 Flask API,主要封装了 mcstatus 包,用于查询 Minecraft 服务器状态,支持Java和基岩,以及附带其他小特性! +这是一个 FastAPI 项目,主要封装了 mcstatus 包,用于查询 Minecraft 服务器状态,支持Java和基岩,以及附带其他小特性! ## 💻用法 @@ -7,6 +7,7 @@ - `GET /java?ip= - (Required)` - 查询 Java 版服务器状态 - `GET - /bedrock?ip= - (Required)` - 查询基岩版服务器状态 +- `Get - /docs ` - FastAPI 的内建文档 ## 📦安装&▶启动 @@ -34,6 +35,7 @@ pdm run app.py pip ```bash +# 请确保使用 Python >= 3.13! pip install -r requirements.txt python app.py ``` @@ -57,7 +59,7 @@ docker run --name mcstatus-api -p 8000:8000 -d mcstatus-api 2. 添加一个基于此项目的服务端(他可能只是一个API Caller,或者是一个Websocket服务器?) 服务端可以调用多个API,并将其返回的信息进行合并并输出,旨在用于检查不同地区的延迟 3. 添加是否默认使用 SRV 解析的变量 -4. *等一切尘埃落定后,我会考虑使用 FastAPI* +4. *等一切尘埃落定后,我会考虑使用 FastAPI* --- **已完成** ## 📞 联系 diff --git a/app.py b/app.py index e3552f4..347f092 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # 重写 Flask-MCMOTD,早期版本用的是面向过程的方式进行写的,一个文件写了400多行,真是要爆了T.T +# 2025/8/28 by Murasame:使用 *Claude Sonnet 4* 重写成了 FastAPI # API -from flask import Flask, request, jsonify -from flask_cors import CORS +from fastapi import FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware # Java版查询模块 from JavaServerStatus import java_status @@ -15,27 +16,33 @@ from dnslookup import dns_lookup # 格式化文本 from FormatData import format_java_data, format_bedrock_data, format_index, format_java_index, format_bedrock_index +app = FastAPI( + title="McStatus API", + description="Minecraft服务器状态查询API", + version="2.0.0" +) -app = Flask(__name__) -app.json.sort_keys = False -app.json.ensure_ascii = False -app.json.mimetype = 'application/json;charset=UTF-8' -app.json.compact = False -CORS(app) +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -@app.route('/') -def index(): +@app.get("/") +async def index(): message = format_index() - return jsonify(message), 200 + return message # Java 服务器状态查询 -@app.route('/java') -def get_java_status(): - ip = request.args.get('ip') +@app.get("/java") +async def get_java_status(ip: str = Query(None, description="服务器IP地址或域名")): # 空值输出 API 用法 if not ip: message = format_java_index() - return jsonify(message), 400 + raise HTTPException(status_code=400, detail=message) try: ip, type = dns_lookup(ip) @@ -44,19 +51,18 @@ def get_java_status(): data = format_java_data(ip, type, status) - return jsonify(data), 200 + return data except Exception as e: - return jsonify({"error": str(e)}), 500 + raise HTTPException(status_code=500, detail=str(e)) # 基岩版服务器状态查询 -@app.route('/bedrock') -def get_bedrock_status(): - ip = request.args.get('ip') +@app.get("/bedrock") +async def get_bedrock_status(ip: str = Query(None, description="服务器IP地址或域名")): # 空值输出 API 用法 if not ip: message = format_bedrock_index() - return jsonify(message), 400 + raise HTTPException(status_code=400, detail=message) try: print(f"解析基岩版IP: {ip}") @@ -64,10 +70,11 @@ def get_bedrock_status(): data = format_bedrock_data(ip, status) - return jsonify(data), 200 + return data except Exception as e: - return jsonify({"error": str(e)}), 500 + raise HTTPException(status_code=500, detail=str(e)) if __name__ == '__main__': - app.run(debug=True, port=5000) \ No newline at end of file + import uvicorn + uvicorn.run("app:app", host="0.0.0.0", port=5000, reload=True) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 78e7116..482b799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "mcstatus-api" version = "0.1.0" -description = "Add your description here" +description = "A Minecraft Server status API based on FastAPI" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "flask>=3.1.1", - "flask-cors>=6.0.1", + "fastapi[standard]>=0.115.0", + "uvicorn>=0.30.0", "mcstatus>=12.0.5", ] diff --git a/requirements.txt b/requirements.txt index 000ae799d930d2b97e6a9a02495698f4a2ba0aea..08c5e7700cc006313469fc45cece28191cb44912 100644 GIT binary patch literal 20773 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 M19vDiSEuUye^R#nx&QzG literal 112 zcmezWFO4CGA(5e&A)7&g!H&TeNE$QfF&Kg|F9R0?LSC04nIWH{2&fjO(hR7|0Io6@ XC|}G_0yM3Jp%h7#A%hWAohbtVI$RLn