1
0
mirror of https://github.com/tzuwei-huang/random-number-generator.git synced 2026-08-24 14:10:29 +09:00
- Replace Flask with FastAPI for improved performance and modern architecture
- Add exhaustive test coverage for language toggle, range settings, and functionality validations
- Ensure app instance uniqueness with process checking logic
- Update dependency management and improve repository state for FastAPI compatibility
This commit is contained in:
TzuWei 2026-01-24 04:33:27 +09:00
parent 713ad34afb
commit 7b3a221e48
Signed by: tzuwei-huang
GPG Key ID: 88A0D3DA7558EE01
10 changed files with 779 additions and 135 deletions

13
.gitignore vendored
View File

@ -1,3 +1,16 @@
# Python
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
ENV/
# PyInstaller
build/
dist/
*.spec
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# Fair Random Number Generator | 公平隨機抽號器
[English](#english) | [繁體中文](#繁體中文)
---
## English
### Description
A web-based random number generator built with FastAPI. It ensures fairness by picking numbers from a pre-defined pool and removing them until the pool is empty. It features a large, easy-to-read UI suitable for presentations or events.
### Features
- **Fair Picking**: Each number is drawn only once from the specified range.
- **Bilingual Support**: Supports both English and Traditional Chinese.
- **Large UI**: Optimized font sizes for high visibility.
- **Session-based**: Keeps track of drawn numbers and remaining pool within the session.
- **Security**: Uses cryptographically strong random numbers (Python's `secrets` module).
- **Fairness Verification**: Includes a script to verify the uniform distribution of results.
### Fairness Mechanism
This app ensures fairness through two layers:
1. **Logical Fairness (Pool-based)**: Instead of just picking a random number, the app creates a "pool" containing all numbers in the specified range. When a number is picked, it is **removed** from the pool. This guarantees that every number is drawn exactly once, and no number is repeated or skipped.
2. **Statistical Fairness (Cryptographic Randomness)**: The app uses Python's `secrets` module (`secrets.randbelow`) to select the index from the pool. Unlike the standard `random` module, `secrets` is designed for cryptography and provides high-quality randomness that is resistant to prediction.
You can run the verification script yourself to see the distribution:
```bash
python tests/verify_fairness.py
```
### Quick Start (Executable)
If you don't want to install Python, you can download the latest version from [GitHub Releases](https://github.com/your-username/your-repo-name/releases).
1. Download `RandomPicker.exe`.
2. Double-click to run it.
3. Open your browser and navigate to `http://127.0.0.1`.
### Installation (From Source)
1. **Requirement**: Python 3.13+ and [uv](https://github.com/astral-sh/uv).
2. **Install dependencies**:
```bash
uv sync
```
### Usage (From Source)
1. **Run the application**:
```bash
uv run exec.py
```
2. Open your browser and navigate to `http://127.0.0.1`.
---
## 繁體中文
### 說明
一個基於 FastAPI 開發的網頁版隨機抽號器。它透過從預定義的池中抽取號碼並移除,直到所有號碼都被抽完,從而確保公平性。具備超大字體介面,非常適合在簡報、抽獎或各種活動中使用。
### 功能特點
- **公平抽籤**:在指定範圍內,每個數字只會被抽中一次。
- **雙語支持**:支援英文與繁體中文介面。
- **超大 UI**:針對高能見度優化的字體大小。
- **會話管理**:在瀏覽器會話中記錄已抽出的號碼與剩餘數量。
- **安全性**使用密碼學等級的隨機數生成器Python 的 `secrets` 模組)。
- **公平性驗證**:內建驗證腳本,可測試結果的均勻分佈。
### 公平性機制
本應用程式透過兩個層面確保公平性:
1. **邏輯公平(基於號碼池)**:程式並非單純隨機抽號,而是先建立一個包含範圍內所有號碼的「號碼池」。每當抽到一個號碼,就會將其從池中**移除**。這保證了每個號碼在一個循環內只會被抽中一次,不會重複也不會遺漏。
2. **統計公平(密碼學等級隨機性)**:程式使用 Python 的 `secrets` 模組 (`secrets.randbelow`) 來從池中挑選索引。與標準的 `random` 模組不同,`secrets` 模組是專為密碼學設計的,提供高品質且難以預測的隨機性。
您可以自行執行驗證腳本來查看分佈情況:
```bash
python tests/verify_fairness.py
```
### 快速開始 (執行檔)
如果您不想安裝 Python可以從 [GitHub Releases](https://github.com/your-username/your-repo-name/releases) 下載最新版本。
1. 下載 `RandomPicker.exe`
2. 雙擊執行該程式。
3. 開啟瀏覽器並造訪 `http://127.0.0.1`
### 安裝步驟 (從原始碼)
1. **需求**Python 3.13+ 以及 [uv](https://github.com/astral-sh/uv)。
2. **安裝依賴**
```bash
uv sync
```
### 使用方式 (從原始碼)
1. **啟動應用程式**
```bash
uv run exec.py
```
2. 開啟瀏覽器並造訪 `http://127.0.0.1`

30
exec.py
View File

@ -1,5 +1,31 @@
from rng_app.app import rng_app
import webbrowser
import uvicorn
import sys
import ctypes
from threading import Timer
from rng_app.app import app
def open_browser():
webbrowser.open_new("http://127.0.0.1")
def is_already_running():
mutex_name = "Global\\RandomPickerMutex_Unique_ID_12345"
kernel32 = ctypes.windll.kernel32
mutex = kernel32.CreateMutexW(None, False, mutex_name)
last_error = kernel32.GetLastError()
if last_error == 183:
return True
return False
if __name__ == "__main__":
rng_app.run(debug=True)
if is_already_running():
open_browser()
sys.exit(0)
Timer(1, open_browser).start()
try:
uvicorn.run(app, host="127.0.0.1", port=80, log_config=None)
except Exception:
open_browser()
sys.exit(1)

View File

@ -1,12 +1,13 @@
import secrets
from typing import Optional
def random_generate_number(min_val: int = 1, max_val: int = 100) -> int:
return secrets.randbelow(max_val - min_val + 1) + min_val
def pick_and_remove(numbers: list[int]) -> tuple[int, list[int]]:
def pick_and_remove(numbers: list[int]) -> tuple[Optional[int], list[int]]:
if not numbers:
return None, []
idx = secrets.randbelow(len(numbers))
picked = numbers.pop(idx)
return picked, numbers

View File

@ -1,8 +1,19 @@
[project]
name = "random-number-generator"
version = "0.1.0"
description = "Add your description here"
version = "1.0.0"
description = "A fair random number generator"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.2",
"fastapi>=0.128.0",
"httpx>=0.28.1",
"itsdangerous>=2.2.0",
"jinja2>=3.1.6",
"pytest>=9.0.2",
"python-multipart>=0.0.21",
"uvicorn>=0.40.0",
]
[dependency-groups]
dev = [
"pyinstaller>=6.18.0",
]

View File

@ -1,6 +1,16 @@
import os
import sys
from typing import Optional
from flask import Flask, request, render_template, session, redirect, url_for
from fastapi import FastAPI, Request, Form
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
def get_resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
TRANSLATIONS = {
"en": {
@ -38,102 +48,109 @@ TRANSLATIONS = {
}
from number_generator.number_generator import pick_and_remove
MAX_RANGE_SIZE = 10000
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
rng_app = Flask("Random Number Generator", template_folder=os.path.join(BASE_DIR, "templates"))
rng_app.secret_key = os.urandom(24)
def initialize_pool(request: Request, min_val: int, max_val: int):
if min_val > max_val:
return
if (max_val - min_val + 1) > MAX_RANGE_SIZE:
return
@rng_app.route("/", methods=["GET", "POST"])
def index():
if "lang" not in session:
session["lang"] = "en"
pool = list(range(min_val, max_val + 1))
request.session["current_range"] = [min_val, max_val]
request.session["is_active"] = True
number = session.get("last_number")
remaining_count = 0
number, updated_pool = pick_and_remove(pool)
request.session["pool"] = updated_pool
request.session["last_number"] = number
request.session["drawn"] = [number]
template_dir = get_resource_path(os.path.join("rng_app", "templates"))
templates = Jinja2Templates(directory=template_dir)
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=os.getenv("SESSION_SECRET_KEY", os.urandom(24)))
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
if "lang" not in request.session:
request.session["lang"] = "zh"
if request.method == "POST":
if "min" in request.form and "max" in request.form:
try:
min_val = int(request.form["min"])
max_val = int(request.form["max"])
if min_val <= max_val:
pool = list(range(min_val, max_val + 1))
session["current_range"] = [min_val, max_val]
session["is_active"] = True
session["drawn"] = []
number, updated_pool = pick_and_remove(pool)
session["pool"] = updated_pool
session["last_number"] = number
session["drawn"].insert(0, number)
session.modified = True
except ValueError:
pass
number = request.session.get("last_number")
pool = request.session.get("pool", [])
remaining_count = len(pool)
lang = request.session["lang"]
elif "pool" in session and session["pool"]:
pool = session["pool"]
number, updated_pool = pick_and_remove(pool)
session["pool"] = updated_pool
session["last_number"] = number
if "drawn" not in session:
session["drawn"] = []
session["drawn"].insert(0, number)
session.modified = True
remaining_count = len(session.get("pool", []))
return render_template("index.html",
number=number,
remaining_count=remaining_count,
has_range=session.get("is_active", False),
range_vals=session.get("current_range"),
drawn=session.get("drawn", []),
t=TRANSLATIONS[session["lang"]],
current_lang=session["lang"])
@rng_app.route("/set_lang/<lang>")
def set_lang(lang):
if lang in TRANSLATIONS:
session["lang"] = lang
return redirect(request.referrer or url_for("index"))
@rng_app.route("/restart")
def restart():
lang = session.get("lang", "en")
current_range = session.get("current_range")
current_range = request.session.get("current_range")
formatted_number = None
formatted_drawn = []
if current_range:
min_val, max_val = current_range
pool = list(range(min_val, max_val + 1))
session["is_active"] = True
session["drawn"] = []
max_val = current_range[1]
padding = len(str(max_val))
if number is not None:
formatted_number = f"{number:0{padding}d}"
drawn = request.session.get("drawn", [])
formatted_drawn = [f"{n:0{padding}d}" for n in drawn]
else:
if number is not None:
formatted_number = str(number)
formatted_drawn = [str(n) for n in request.session.get("drawn", [])]
return templates.TemplateResponse("index.html", {
"request": request,
"number": formatted_number,
"remaining_count": remaining_count,
"has_range": request.session.get("is_active", False),
"range_vals": current_range,
"drawn": formatted_drawn,
"t": TRANSLATIONS.get(lang, TRANSLATIONS["en"]),
"current_lang": lang
})
@app.post("/")
async def handle_post(request: Request, min: Optional[int] = Form(None), max: Optional[int] = Form(None)):
if min is not None and max is not None:
initialize_pool(request, min, max)
elif "pool" in request.session and request.session["pool"]:
pool = request.session["pool"]
number, updated_pool = pick_and_remove(pool)
session["pool"] = updated_pool
session["last_number"] = number
session["drawn"].insert(0, number)
session.modified = True
session["lang"] = lang
return redirect(url_for("index"))
@rng_app.route("/reset")
def reset():
lang = session.get("lang", "en")
current_range = session.get("current_range")
session.clear()
session["lang"] = lang
session["last_number"] = None
session["is_active"] = False
if current_range:
session["current_range"] = current_range
request.session["pool"] = updated_pool
request.session["last_number"] = number
return redirect(url_for("index"))
drawn = request.session.get("drawn", [])
if number is not None:
drawn.insert(0, number)
request.session["drawn"] = drawn
return RedirectResponse(url="/", status_code=303)
@app.get("/set_lang/{lang}")
async def set_lang(request: Request, lang: str):
if lang in TRANSLATIONS:
request.session["lang"] = lang
return RedirectResponse(url="/", status_code=303)
@app.get("/restart")
async def restart(request: Request):
current_range = request.session.get("current_range")
if current_range:
initialize_pool(request, current_range[0], current_range[1])
return RedirectResponse(url="/", status_code=303)
@app.get("/reset")
async def reset(request: Request):
lang = request.session.get("lang", "zh")
current_range = request.session.get("current_range")
request.session.clear()
request.session["lang"] = lang
request.session["is_active"] = False
if current_range:
request.session["current_range"] = current_range
return RedirectResponse(url="/", status_code=303)

View File

@ -3,25 +3,25 @@
<head>
<title>Fair Random Number Generator</title>
<style>
body { font-family: sans-serif; text-align: center; margin-top: 50px; background-color: #f4f7f6; }
.container { max-width: 600px; margin: 0 auto; padding: 40px; background: white; border: 1px solid #eee; border-radius: 15px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); position: relative; }
body { font-family: sans-serif; text-align: center; margin: 20px 0; background-color: #f4f7f6; font-size: 1.2em; }
.container { width: 98%; max-width: 98%; margin: 0 auto; padding: 40px 20px; background: white; border: 1px solid #eee; border-radius: 15px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); position: relative; box-sizing: border-box; }
.lang-switch { position: absolute; top: 20px; right: 20px; }
.lang-link { text-decoration: none; color: #3498db; font-size: 1.1em; font-weight: bold; }
h1 { color: #2c3e50; margin-bottom: 30px; font-size: 2.5em; }
.result { font-size: 8em; font-weight: bold; color: #e74c3c; margin: 30px 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.1); }
.info { font-size: 1.5em; color: #7f8c8d; margin-bottom: 30px; }
button { padding: 15px 30px; font-size: 1.5em; cursor: pointer; transition: all 0.2s; }
.lang-link { text-decoration: none; color: #3498db; font-size: 1.2em; font-weight: bold; }
h1 { color: #2c3e50; margin-bottom: 30px; font-size: 3em; }
.result { font-size: 16em; font-weight: bold; color: #e74c3c; margin: 30px 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.1); }
.info { font-size: 2em; color: #7f8c8d; margin-bottom: 30px; }
button { padding: 20px 40px; font-size: 2em; cursor: pointer; transition: all 0.2s; }
button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
button:active { transform: translateY(0); }
.reset-btn { background-color: #f8f9fa; border: 1px solid #dee2e6; color: #6c757d; text-decoration: none; padding: 12px 25px; border-radius: 6px; display: inline-block; margin-top: 20px; font-size: 1.1em; }
.reset-btn { background-color: #f8f9fa; border: 1px solid #dee2e6; color: #6c757d; text-decoration: none; padding: 15px 30px; border-radius: 6px; display: inline-block; margin-top: 20px; font-size: 1.3em; }
.reset-btn:hover { background-color: #e9ecef; }
.generate-btn { background-color: #2ecc71; border: none; color: white; border-radius: 6px; font-weight: bold; }
.generate-btn:hover { background-color: #27ae60; }
input[type="number"] { padding: 10px; border: 2px solid #edeff2; border-radius: 8px; width: 100px; outline: none; font-size: 1.5em; text-align: center; }
input[type="number"] { padding: 15px; border: 2px solid #edeff2; border-radius: 8px; width: 150px; outline: none; font-size: 2em; text-align: center; }
input[type="number"]:focus { border-color: #3498db; }
label { font-size: 1.5em; color: #34495e; margin-right: 5px; }
label { font-size: 2em; color: #34495e; margin-right: 10px; }
.drawn-list { margin-top: 40px; text-align: left; background: #fdfdfd; padding: 25px; border: 1px dashed #dcdde1; border-radius: 10px; }
.drawn-numbers { font-size: 1.4em; font-weight: bold; color: #34495e; word-wrap: break-word; line-height: 1.6; margin-top: 10px; }
.drawn-numbers { font-size: 3em; font-weight: bold; color: #34495e; word-wrap: break-word; line-height: 1.2; margin-top: 10px; }
</style>
</head>
<body>
@ -37,11 +37,11 @@
{% if not has_range %}
<form method="post">
<p style="font-size: 1.5em; color: #34495e;">{{ t.enter_range }}</p>
<p style="font-size: 2em; color: #34495e;">{{ t.enter_range }}</p>
<label>{{ t.min }}</label>
<input type="number" name="min" value="{{ range_vals[0] if range_vals else 1 }}" required>
<input type="number" name="min" value="{{ range_vals[0] if range_vals else 1 }}" min="0" required>
<label>{{ t.max }}</label>
<input type="number" name="max" value="{{ range_vals[1] if range_vals else 100 }}" required>
<input type="number" name="max" value="{{ range_vals[1] if range_vals else 100 }}" min="0" required>
<br><br>
<button type="submit" class="generate-btn">{{ t.start }}</button>
</form>
@ -61,8 +61,8 @@
{% if remaining_count > 0 %}
<button type="submit" class="generate-btn">{{ t.pick_one }}</button>
{% else %}
<p style="font-size: 1.5em; color: #2ecc71; font-weight: bold;">{{ t.all_picked }}</p>
<a href="/restart" class="generate-btn" style="text-decoration: none; display: inline-block; padding: 15px 30px;">{{ t.restart }}</a>
<p style="font-size: 2em; color: #2ecc71; font-weight: bold;">{{ t.all_picked }}</p>
<a href="/restart" class="generate-btn" style="text-decoration: none; display: inline-block; padding: 20px 40px;">{{ t.restart }}</a>
{% endif %}
</form>
@ -74,7 +74,7 @@
{% if drawn %}
<div class="drawn-list">
<strong style="font-size: 1.2em;">{{ t.drawn_numbers }}</strong>
<strong style="font-size: 2em;">{{ t.drawn_numbers }}</strong>
<div class="drawn-numbers">{{ drawn | join(', ') }}</div>
</div>
{% endif %}

82
tests/test_app.py Normal file
View File

@ -0,0 +1,82 @@
from fastapi.testclient import TestClient
from rng_app.app import app
import pytest
import re
client = TestClient(app)
def test_index_default_lang():
response = client.get("/")
assert response.status_code == 200
# 預設應為繁體中文
assert "隨機抽號器" in response.text
assert "輸入範圍" in response.text
def test_language_toggle():
# 切換到英文
response = client.get("/set_lang/en", follow_redirects=True)
assert "Random Picker" in response.text
assert "Enter the range:" in response.text
# 切換回中文
response = client.get("/set_lang/zh", follow_redirects=True)
assert "隨機抽號器" in response.text
def test_set_range_with_padding():
# 設定範圍 1-100 (補足 3 位)
response = client.post("/", data={"min": 1, "max": 100}, follow_redirects=True)
assert response.status_code == 200
assert "範圍: 1 ~ 100" in response.text
# 第一個號碼應該補足 3 位
match = re.search(r'<div class="result">\s*(\d{3})\s*</div>', response.text)
assert match is not None
print(f"Padded number: {match.group(1)}")
def test_drawn_list_padding():
client.post("/", data={"min": 1, "max": 10}, follow_redirects=True)
response = client.post("/", follow_redirects=True)
match = re.search(r'<div class="drawn-numbers">([\d, ]+)</div>', response.text)
assert match is not None
drawn_str = match.group(1)
numbers = [n.strip() for n in drawn_str.split(',')]
assert len(numbers) >= 2
for n in numbers:
assert len(n) == 2
def test_full_cycle_and_all_picked():
client.post("/", data={"min": 1, "max": 2}, follow_redirects=True)
response = client.post("/", follow_redirects=True)
assert "剩餘數量: 0" in response.text
assert "全部抽完啦!" in response.text
response = client.post("/", follow_redirects=True)
assert "全部抽完啦!" in response.text
def test_restart_functionality():
client.post("/", data={"min": 1, "max": 10}, follow_redirects=True)
client.post("/", follow_redirects=True)
client.post("/", follow_redirects=True)
response = client.get("/restart", follow_redirects=True)
assert "剩餘數量: 9" in response.text # 重新初始化後抽出第 1 個
assert "範圍: 1 ~ 10" in response.text
def test_reset_functionality():
client.post("/", data={"min": 1, "max": 10}, follow_redirects=True)
response = client.get("/reset", follow_redirects=True)
assert "輸入範圍:" in response.text
assert 'class="info"' not in response.text
def test_invalid_range():
response = client.post("/", data={"min": 10, "max": 1}, follow_redirects=True)
assert 'class="info"' not in response.text
def test_max_range_limit():
response = client.post("/", data={"min": 1, "max": 10001}, follow_redirects=True)
assert 'class="info"' not in response.text
if __name__ == "__main__":
import sys
pytest.main([__file__])

39
tests/verify_fairness.py Normal file
View File

@ -0,0 +1,39 @@
import secrets
from collections import Counter
import math
def pick_and_remove(numbers: list[int]) -> tuple[int, list[int]]:
if not numbers:
return None, []
idx = secrets.randbelow(len(numbers))
picked = numbers.pop(idx)
return picked, numbers
def test_fairness(range_size=10, iterations=100000):
print(f"Testing fairness over {iterations} iterations with range 1-{range_size}...")
# We want to check if the first number picked in each session is uniformly distributed
first_picks = []
for _ in range(iterations):
pool = list(range(1, range_size + 1))
picked, _ = pick_and_remove(pool)
first_picks.append(picked)
counts = Counter(first_picks)
expected = iterations / range_size
print(f"{'Number':<10} | {'Count':<10} | {'Deviation':<10}")
print("-" * 35)
for i in range(1, range_size + 1):
count = counts[i]
deviation = (count - expected) / expected * 100
print(f"{i:<10} | {count:<10} | {deviation:>8.2f}%")
# Chi-squared test for goodness of fit
chi_sq = sum((counts[i] - expected)**2 / expected for i in range(1, range_size + 1))
print(f"\nChi-squared statistic: {chi_sq:.4f}")
# For df = 9, p=0.05, critical value is 16.919
print("For 10 categories, a value below 16.92 suggests the distribution is uniform (95% confidence).")
if __name__ == "__main__":
test_fairness()

396
uv.lock generated
View File

@ -3,12 +3,51 @@ revision = 1
requires-python = ">=3.13"
[[package]]
name = "blinker"
version = "1.9.0"
name = "altgraph"
version = "0.17.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228 },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 },
]
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 },
]
[[package]]
@ -33,20 +72,73 @@ wheels = [
]
[[package]]
name = "flask"
version = "3.1.2"
name = "fastapi"
version = "0.128.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160 }
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308 },
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094 },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
]
[[package]]
@ -70,6 +162,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "macholib"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117 },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
@ -122,25 +226,275 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
]
[[package]]
name = "pefile"
version = "2024.8.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766 },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
]
[[package]]
name = "pyinstaller"
version = "6.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/b8/0fe3359920b0a4e7008e0e93ff383003763e3eee3eb31a07c52868722960/pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9", size = 4034976 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/e6/51b0146a1a3eec619e58f5d69fb4e3d0f65a31cbddbeef557c9bb83eeed9/pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3", size = 1040056 },
{ url = "https://files.pythonhosted.org/packages/4c/9c/a3634c0ec8e1ed31b373b548848b5c0b39b56edc191cf737e697d484ec23/pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33", size = 734971 },
{ url = "https://files.pythonhosted.org/packages/2c/04/6756442078ccfcd552ccce636be1574035e62f827ffa1f5d8a0382682546/pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37", size = 746637 },
{ url = "https://files.pythonhosted.org/packages/54/39/fbc56519000cdbf450f472692a7b9b55d42077ce8529f1be631db7b75a36/pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387", size = 744343 },
{ url = "https://files.pythonhosted.org/packages/36/f2/50887badf282fee776e83d1e4feab74c026f50a1ea16e109ed939e32aa28/pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af", size = 741084 },
{ url = "https://files.pythonhosted.org/packages/1c/08/3a1419183e4713ef77d912ecbdd6ef858689ed9deb34d547133f724ca745/pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b", size = 740943 },
{ url = "https://files.pythonhosted.org/packages/c2/47/309305e36d116f1434b42d91c420ff951fa79b2c398bbd59930c830450be/pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e", size = 740107 },
{ url = "https://files.pythonhosted.org/packages/83/0f/a59a95cd1df59ddbc9e74d5a663387551333bcf19a5dd3086f5c81a2e83c/pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3", size = 739843 },
{ url = "https://files.pythonhosted.org/packages/9a/09/e7a870e7205cdbd2f8785010a5d3fe48a9df2591156ee34a8b29b774fa14/pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b", size = 1323811 },
{ url = "https://files.pythonhosted.org/packages/fb/d5/48eef2002b6d3937ceac2717fe17e9ca3a43a4c9826bafee367dfc75ba85/pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9", size = 1384389 },
{ url = "https://files.pythonhosted.org/packages/1b/8d/1a88e6e94107de3ea1c842fd59c3aa132d344ad8e52ea458ffa9a748726e/pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381", size = 1324869 },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2026.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318 },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 },
]
[[package]]
name = "python-multipart"
version = "0.0.21"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541 },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
]
[[package]]
name = "random-number-generator"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "pytest" },
{ name = "python-multipart" },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
{ name = "pyinstaller" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [{ name = "flask", specifier = ">=3.1.2" }]
requires-dist = [
{ name = "fastapi", specifier = ">=0.128.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "itsdangerous", specifier = ">=2.2.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "python-multipart", specifier = ">=0.0.21" },
{ name = "uvicorn", specifier = ">=0.40.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pyinstaller", specifier = ">=6.18.0" },
{ name = "pytest", specifier = ">=9.0.2" },
]
[[package]]
name = "werkzeug"
version = "3.1.5"
name = "setuptools"
version = "80.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859 },
]
[[package]]
name = "starlette"
version = "0.50.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754 }
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025 },
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
]
[[package]]
name = "uvicorn"
version = "0.40.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 },
]