diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..782a267 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.log +*.sqlite3 +.env +.venv/ +venv/ +.git/ +.gitignore +node_modules/ +uv.lock diff --git a/.gitea/changelog-config.json b/.gitea/changelog-config.json new file mode 100644 index 0000000..88b580b --- /dev/null +++ b/.gitea/changelog-config.json @@ -0,0 +1,114 @@ +{ + "categories": [ + { + "title": "## 🚀 Features", + "labels": [ + "add", + "Add", + "Kind/Feature", + "feat", + "Feature", + "Feat" + ] + }, + { + "title": "## 🧰 Enhancements", + "labels": [ + "enhancement", + "Enhancement", + "Kind/Enhancement", + "improvement", + "Improvement", + "Kind/Improvement" + ] + }, + { + "title": "## 🐛 Fixes", + "labels": [ + "fix", + "Fix", + "Kind/Bug", + "Kind/Security" + ] + }, + { + "title": "## 🧪 Upgrade", + "labels": [ + "upgrade", + "Upgrade", + "Clean" + ] + }, + { + "title": "## 📝 Documentation", + "labels": [ + "docs", + "Docs", + "Kind/Documentation" + ] + }, + { + "title": "## 🛠️ Maintenance", + "labels": [ + "maintenance", + "Maintenance", + "Kind/Maintenance", + "chore", + "Chore", + "Kind/Chore" + ] + }, + { + "title": "## ⏪ Reverts", + "labels": [ + "revert", + "Revert", + "Kind/Revert", + "Kind/Reverts", + "reverts", + "Reverts" + ] + }, + { + "title": "## 🗑️ Deprecation", + "labels": [ + "deprecation", + "Deprecation", + "Kind/Deprecation" + ] + }, + { + "title": "## ⚡️ Performance Improvements", + "labels": [ + "perf", + "Perf", + "Kind/Performance" + ] + }, + { + "title": "## 🎨 Styling", + "labels": [ + "style", + "Style", + "Kind/Style" + ] + }, + { + "title": "## 🎯 Other Changes", + "labels": [] + } + ], + "label_extractor": [ + { + "pattern": "(\\w+) (.+)", + "target": "$1", + "on_property": "title" + } + ], + "sort": "ASC", + "template": "${{CHANGELOG}}", + "pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}", + "empty_template": "- no changes", + "max_pull_requests": 1000, + "max_back_track_time_days": 1000 +} \ No newline at end of file diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..cc66507 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,117 @@ +on: + workflow_dispatch: + inputs: + github_release: + description: 'Create Gitea Release' + default: true + type: boolean + docker_release: + description: 'Push Docker images' + default: true + type: boolean + bump: + description: 'Bump type' + required: true + default: 'patch' + type: choice + options: + - 'major' + - 'minor' + - 'patch' + + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@master + with: + fetch-depth: 0 + fetch-tags: true + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Set up Python + run: uv python install + with: + python-version-file: "pyproject.toml" + + - name: Install the project + run: uv sync --all-extras --dev + + - name: Create requirements.txt + run: | + echo "Creating requirements.txt" + uv export --format requirements.txt -o requirements.txt + echo "requirements.txt created" + + - name: Set Git identity + run: | + git config user.name "Gitea CI" + git config user.email "ci@git.theprivateserver.de" + + - name: Build Changelog + id: build_changelog + uses: https://github.com/mikepenz/release-changelog-builder-action@v5 + with: + platform: "gitea" + baseURL: "http://gitea:3000" + configuration: ".gitea/changelog-config.json" + env: + GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} + + + - name: Bump version + id: bump + run: | + uv tool install bump-my-version + uv tool run bump-my-version bump ${{ github.event.inputs.bump }} --allow-dirty + # echo the version to github env, the version is shown by using uv tool run bump-my-version show current_version + echo "VERSION<> $GITHUB_ENV + echo "$(uv tool run bump-my-version show current_version)" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Check version + run: echo ${{ env.VERSION }} + - name: Build and store Docker image + if: ${{ github.event.inputs.docker_release == 'true' }} + run: | + REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + docker buildx build \ + --platform linux/amd64 \ + --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:latest \ + --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:${{ env.VERSION }} \ + --push . + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref }} + - name: Create release + id: create_release + if: ${{ github.event.inputs.github_release == 'true' }} + uses: softprops/action-gh-release@master + with: + tag_name: v${{ env.VERSION }} + release_name: Release ${{ env.VERSION }} + body: ${{steps.build_changelog.outputs.changelog}} + draft: false + prerelease: false + make_latest: true + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} + diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100755 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f16a5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Install system deps (optional: to build some wheels faster). Kept minimal here. +WORKDIR /app + +# Copy dependency list and install first (leverages Docker layer cache) +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY app ./app + +EXPOSE 8000 + +# Run the FastAPI app with Uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/main.py b/app/main.py new file mode 100755 index 0000000..8930e82 --- /dev/null +++ b/app/main.py @@ -0,0 +1,88 @@ +import os +import re +import smtplib +from email.mime.text import MIMEText +from xml.etree.ElementTree import Element, SubElement, tostring + +from fastapi import FastAPI, Form, HTTPException, Request, status +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +app = FastAPI() +templates = Jinja2Templates(directory="app/templates") + +# Serve static files (CSS, images) +app.mount("/static", StaticFiles(directory="app/static"), name="static") + +# add somewhere near the top-level constants +EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") + +# SMTP / email configuration via environment +SMTP_HOST = os.getenv("SMTP_HOST", "smtp") +SMTP_PORT = int(os.getenv("SMTP_PORT", "25")) +MAIL_FROM = os.getenv("MAIL_FROM", "noreply@example.com") +MAIL_TO = os.getenv("MAIL_TO", "destination@example.com") + + +@app.get("/", response_class=HTMLResponse) +async def show_form(request: Request): + return templates.TemplateResponse("form.html", {"request": request}) + + +@app.post("/submit") +async def handle_form( + request: Request, + name: str = Form(...), + lastname: str = Form(...), + title: str = Form(...), + telno: str = Form(...), + mail: str = Form(...), + apparatsname: str = Form(...), + subject: str = Form(...), + semester_type: str = Form(...), # "summer" or "winter" + semester_year: str = Form(...), + authorname: list[str] = Form(...), + year: list[str] = Form(...), + booktitle: list[str] = Form(...), + signature: list[str] = Form(...), +): + # Build XML + root = Element("form_submission") + + static_data = SubElement(root, "static") + SubElement(static_data, "name").text = name + SubElement(static_data, "lastname").text = lastname + SubElement(static_data, "title").text = title + SubElement(static_data, "telno").text = telno + SubElement(static_data, "mail").text = mail + SubElement(static_data, "apparatsname").text = apparatsname + SubElement(static_data, "subject").text = subject + SubElement(static_data, "semester").text = f"{semester_type} {semester_year}" + # inside handle_form(), right after parameters are received, before building XML: + # Basic email validation (server-side) + if not EMAIL_REGEX.match(mail): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid email address format.", + ) + books = SubElement(root, "books") + for i in range(len(authorname)): + book = SubElement(books, "book") + SubElement(book, "authorname").text = authorname[i] + SubElement(book, "year").text = year[i] + SubElement(book, "title").text = booktitle[i] + SubElement(book, "signature").text = signature[i] + + xml_data = tostring(root, encoding="unicode") + + # Send mail + msg = MIMEText(xml_data, "xml") + msg["Subject"] = "New Form Submission" + msg["From"] = MAIL_FROM + msg["To"] = MAIL_TO + + with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: + server.send_message(msg) + + return RedirectResponse("/", status_code=303) diff --git a/app/static/styles.css b/app/static/styles.css new file mode 100644 index 0000000..e519780 --- /dev/null +++ b/app/static/styles.css @@ -0,0 +1,291 @@ +:root { + --bg: #f7f8fb; + --card-bg: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --primary: #0b7bd6; /* Accessible blue */ + --primary-600: #0a6ec0; + --primary-700: #095fa6; + --border: #e5e7eb; + --ring: rgba(11, 123, 214, 0.35); + --table-head-bg: #f3f4f6; + --input-bg: #ffffff; + --control-accent: var(--primary); +} + +/* Dark theme variables */ +[data-theme="dark"] { + --bg: #0b1220; + --card-bg: #121a2a; + --text: #e5e7eb; + --muted: #9aa4b2; + --primary: #5aa1ff; + --primary-600: #4b90ea; + --primary-700: #3c7ace; + --border: #243045; + --ring: rgba(90, 161, 255, 0.35); + --table-head-bg: #172136; + --input-bg: #0f1726; + --control-accent: #2a3448; +} + +* { box-sizing: border-box; } + +html, body { height: 100%; } + +body { + margin: 0; + font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; + color: var(--text); + background: var(--bg); + line-height: 1.5; +} + +/* Header */ +.site-header { + background: var(--card-bg); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} +.header-inner { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 0; + justify-content: space-between; +} +.header-left { + display: flex; + align-items: center; + gap: 14px; +} +.logo { height: 48px; width: auto; } +.brand-title { font-weight: 700; letter-spacing: .2px; } +.brand-sub { color: var(--muted); font-size: .95rem; } + +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + /* padding: 8px 10px; */ + scale: 2; + border-radius: 9999px; + border: none;/*1px solid var(--border); */ + background: var(--card-bg); + color: var(--text); + cursor: pointer; +} +.theme-toggle:hover { filter: brightness(1.05); } +.theme-toggle:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--ring); } + +/* Icon emphasis depending on theme */ +[data-theme="light"] .theme-toggle .sun { opacity: 1; } +[data-theme="light"] .theme-toggle .moon { opacity: 0.5; } +[data-theme="dark"] .theme-toggle .sun { opacity: 0.5; } +[data-theme="dark"] .theme-toggle .moon { opacity: 1; } + +/* Layout */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.card { + background: var(--card-bg); + margin: 24px 0; + padding: 22px; + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: 0 8px 30px rgba(0,0,0,.05); +} + +h1 { + margin: 0 0 14px; + font-size: 1.5rem; +} + +h2 { + margin: 20px 0 10px; + font-size: 1.25rem; +} + +/* Tables */ +.table-wrapper { overflow-x: auto; } + +.form-table, +.data-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + background: var(--card-bg); + /* Ensure full rounded corners and visible outer border */ + border: none; + border-radius: 12px; + overflow: hidden; + position: relative; +} + +/* Draw the outer border inside the rounded area to avoid clipping */ +.form-table::after, +.data-table::after { + content: ""; + position: absolute; + inset: 0; + border: 1px solid var(--border); + border-radius: 12px; + pointer-events: none; +} + +.form-table td, +.form-table th { + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} + +.data-table td, +.data-table th { + padding: 10px 12px; + border-bottom: 0; +} + +.form-table tr:last-child td, +.data-table tr:last-child td { border-bottom: 0; } + +.data-table th { + background: var(--table-head-bg); + text-align: left; + font-weight: 600; +} + +/* Inputs */ +input[type="text"], +input[type="email"], +input[type="number"], +select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--input-bg); + color: var(--text); + outline: none; + transition: border-color .2s ease, box-shadow .2s ease; +} + +input:focus, +select:focus { + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--ring); +} + +input[type="radio"] { accent-color: var(--control-accent); } + +.radio { margin-right: 12px; } + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border-radius: 10px; + border: 1px solid transparent; + cursor: pointer; + font-weight: 600; + transition: background-color .2s ease, color .2s ease, border-color .2s ease, transform .02s ease; +} + +.btn:active { transform: translateY(1px); } + +.btn-primary { + background: var(--primary); + color: #fff; +} +.btn-primary:hover { background: var(--primary-600); } +.btn-primary:focus { box-shadow: 0 0 0 4px var(--ring); } + +.btn-secondary { + background: #e7eef8; + color: var(--primary-700); + border-color: #cfd9ea; +} +.btn-secondary:hover { background: #dfe8f6; } + +[data-theme="dark"] .btn-secondary { + background: #1c2637; + color: var(--text); + border-color: #2a3852; +} +[data-theme="dark"] .btn-secondary:hover { background: #1f2b3f; } + +.actions { + margin-top: 16px; + display: flex; + justify-content: flex-end; +} + +.footer-note { + margin-top: 18px; + color: var(--muted); +} +.footer-note a { color: var(--primary-700); text-decoration: none; } +.footer-note a:hover { text-decoration: underline; } + +/* Responsive */ +@media (max-width: 720px) { + .header-inner { padding: 12px 0; } + .logo { height: 40px; } + .form-table td:first-child { width: 40%; } +} + +/* Static Information grid */ +.grid-form { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px 16px; /* row x column gap */ + margin-bottom: 16px; +} +.form-field { + display: flex; + flex-direction: column; + gap: 6px; +} +.form-field label { + font-size: 0.9rem; + color: var(--muted); +} +/* Inline info message under Apparatsname */ +.field-info { + margin-top: 4px; + font-size: 0.85rem; + color: var(--primary-700); +} +[data-theme="dark"] .field-info { color: #a9c8ff; } +.hidden { display: none !important; } +.inline-controls { + display: flex; + align-items: center; + gap: 12px; + justify-content: center; + text-align: center; +} +.inline-controls input[type="number"] { + width: 120px; + text-align: center; +} + +/* Responsive tweaks for the grid */ +@media (max-width: 1024px) { + .grid-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 640px) { + .grid-form { grid-template-columns: 1fr; } + .inline-controls { flex-wrap: wrap; } +} + +/* Darken secondary button and add spacing after tables */ +.data-table + .btn { margin-top: 14px; } diff --git a/app/templates/form.html b/app/templates/form.html new file mode 100755 index 0000000..ad0ecd2 --- /dev/null +++ b/app/templates/form.html @@ -0,0 +1,197 @@ + + + + Semesterapparatsantrag + + + + + + + + +
+
+

Static Information

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + + +
+
+ + +
+
+ +
+ + + +
+
+
+ +

Medien

+ + + + + + + + + + +
AutorennameJahr/AuflageTitelSignatur
+ + +
+ +
+
+ + +
+
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6512849 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + web: + build: . + container_name: semapform + environment: + - SMTP_HOST=${SMTP_HOST:-smtp} + - SMTP_PORT=${SMTP_PORT:-25} + - MAIL_FROM=${MAIL_FROM:-noreply@example.com} + - MAIL_TO=${MAIL_TO:-destination@example.com} + ports: + - "8000:8000" + restart: unless-stopped diff --git a/main.py b/main.py new file mode 100644 index 0000000..f0d68d3 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +from app.main import app + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=5001) diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..6afae0e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "semapform" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.117.1", + "jinja2>=3.1.6", + "python-multipart>=0.0.20", + "uvicorn>=0.37.0", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c8037ea --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.117.1 +jinja2>=3.1.6 +python-multipart>=0.0.20 +uvicorn>=0.37.0 diff --git a/uv.lock b/uv.lock new file mode 100755 index 0000000..45e4604 --- /dev/null +++ b/uv.lock @@ -0,0 +1,243 @@ +version = 1 +requires-python = ">=3.13" + +[[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.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "fastapi" +version = "0.117.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959 }, +] + +[[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 = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +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 = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +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/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "semapform" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.117.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "uvicorn", specifier = ">=0.37.0" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736 }, +] + +[[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.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976 }, +]