feat: add rich test runner with progress display
- Add scripts/test-runner.py with rich progress bars and fail-fast behavior - Add rich>=13.0.0 as dev dependency - Update Makefile: `make test` now runs all tests (unit + integration) - Test runner shows live progress, current test, and summary Now `make test` runs both unit and integration tests with docker containers, matching the docker-service-architecture skill guidelines.
This commit is contained in:
14
Makefile
14
Makefile
@@ -1,20 +1,20 @@
|
||||
.PHONY: help test test-unit test-integration build dev-up dev-down pre-deploy clean
|
||||
|
||||
VERBOSE ?= 0
|
||||
PYTEST_ARGS := $(if $(filter 1,$(VERBOSE)),-v,-q)
|
||||
|
||||
# Default target
|
||||
help: ## Show this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# Testing
|
||||
test: test-unit ## Run all tests (unit only by default)
|
||||
test: ## Run all tests (unit + integration) with rich progress display
|
||||
@uv run python scripts/test-runner.py $(if $(filter 1,$(VERBOSE)),-v)
|
||||
|
||||
test-unit: ## Run unit tests
|
||||
uv run pytest tests/unit/ $(PYTEST_ARGS)
|
||||
test-unit: ## Run unit tests only
|
||||
@uv run python scripts/test-runner.py --unit-only $(if $(filter 1,$(VERBOSE)),-v)
|
||||
|
||||
test-integration: ## Run integration tests (starts/stops containers)
|
||||
./scripts/run-integration-tests.sh
|
||||
test-integration: ## Run integration tests only (starts/stops containers)
|
||||
@uv run python scripts/test-runner.py --integration-only $(if $(filter 1,$(VERBOSE)),-v)
|
||||
|
||||
# Docker
|
||||
build: ## Build Docker image
|
||||
@@ -27,7 +27,7 @@ dev-down: ## Stop development environment
|
||||
cd deploy/dev && docker compose down
|
||||
|
||||
# Pre-deployment
|
||||
pre-deploy: test-unit test-integration ## Full pre-deployment pipeline
|
||||
pre-deploy: test ## Full pre-deployment pipeline
|
||||
@echo "Pre-deployment checks passed!"
|
||||
|
||||
# Cleanup
|
||||
|
||||
@@ -17,6 +17,8 @@ dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
"pytest-httpx>=0.32.0",
|
||||
"pytest-timeout>=2.0.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
248
scripts/test-runner.py
Executable file
248
scripts/test-runner.py
Executable file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rich test runner with progress display and fail-fast behavior.
|
||||
|
||||
Runs unit tests, then integration tests with real-time progress indication.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
PASSED = "passed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestStage:
|
||||
name: str
|
||||
command: list[str]
|
||||
status: Status = Status.PENDING
|
||||
progress: int = 0
|
||||
total: int = 0
|
||||
passed: int = 0
|
||||
failed: int = 0
|
||||
current_test: str = ""
|
||||
duration: float = 0.0
|
||||
output: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Regex patterns for parsing pytest output
|
||||
PYTEST_PROGRESS = re.compile(r"\[\s*(\d+)%\]")
|
||||
PYTEST_COLLECTING = re.compile(r"collected (\d+) items?")
|
||||
PYTEST_RESULT = re.compile(r"(\d+) passed")
|
||||
PYTEST_FAILED = re.compile(r"(\d+) failed")
|
||||
PYTEST_DURATION = re.compile(r"in ([\d.]+)s")
|
||||
PYTEST_TEST_LINE = re.compile(r"(tests/\S+::\S+)")
|
||||
|
||||
|
||||
class TestRunner:
|
||||
def __init__(self, verbose: bool = False):
|
||||
self.console = Console()
|
||||
self.verbose = verbose
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
self.stages: list[TestStage] = []
|
||||
self.all_passed = True
|
||||
|
||||
def add_stage(self, name: str, command: list[str]) -> None:
|
||||
self.stages.append(TestStage(name=name, command=command))
|
||||
|
||||
def render_table(self) -> Table:
|
||||
table = Table(show_header=False, box=None, padding=(0, 1))
|
||||
table.add_column("Status", width=3)
|
||||
table.add_column("Name", width=20)
|
||||
table.add_column("Progress", width=30)
|
||||
table.add_column("Time", width=8)
|
||||
|
||||
for stage in self.stages:
|
||||
# Status icon
|
||||
if stage.status == Status.PENDING:
|
||||
icon = Text("○", style="dim")
|
||||
elif stage.status == Status.RUNNING:
|
||||
icon = Text("●", style="yellow")
|
||||
elif stage.status == Status.PASSED:
|
||||
icon = Text("✓", style="green")
|
||||
else:
|
||||
icon = Text("✗", style="red")
|
||||
|
||||
# Progress display
|
||||
if stage.status == Status.PENDING:
|
||||
progress = Text("pending", style="dim")
|
||||
elif stage.status == Status.RUNNING:
|
||||
if stage.total > 0:
|
||||
bar_width = 20
|
||||
filled = int(bar_width * stage.progress / 100)
|
||||
bar = "━" * filled + "░" * (bar_width - filled)
|
||||
progress = Text(f"{bar} {stage.progress:3d}% {stage.passed}/{stage.total}")
|
||||
if stage.current_test:
|
||||
progress.append(f"\n → {stage.current_test[:40]}", style="dim")
|
||||
else:
|
||||
progress = Text("collecting...", style="yellow")
|
||||
elif stage.status == Status.PASSED:
|
||||
progress = Text(f"{stage.passed}/{stage.total}", style="green")
|
||||
else:
|
||||
progress = Text(f"{stage.passed}/{stage.total} ({stage.failed} failed)", style="red")
|
||||
|
||||
# Duration
|
||||
if stage.duration > 0:
|
||||
duration = Text(f"{stage.duration:.1f}s", style="dim")
|
||||
else:
|
||||
duration = Text("")
|
||||
|
||||
table.add_row(icon, stage.name, progress, duration)
|
||||
|
||||
return table
|
||||
|
||||
def parse_output(self, stage: TestStage, line: str) -> None:
|
||||
"""Parse pytest output line and update stage state."""
|
||||
stage.output.append(line)
|
||||
|
||||
# Check for collected count
|
||||
match = PYTEST_COLLECTING.search(line)
|
||||
if match:
|
||||
stage.total = int(match.group(1))
|
||||
|
||||
# Check for progress percentage
|
||||
match = PYTEST_PROGRESS.search(line)
|
||||
if match:
|
||||
stage.progress = int(match.group(1))
|
||||
# Estimate passed based on progress
|
||||
if stage.total > 0:
|
||||
stage.passed = int(stage.total * stage.progress / 100)
|
||||
|
||||
# Check for current test
|
||||
match = PYTEST_TEST_LINE.search(line)
|
||||
if match:
|
||||
stage.current_test = match.group(1)
|
||||
|
||||
# Check for final results
|
||||
match = PYTEST_RESULT.search(line)
|
||||
if match:
|
||||
stage.passed = int(match.group(1))
|
||||
|
||||
match = PYTEST_FAILED.search(line)
|
||||
if match:
|
||||
stage.failed = int(match.group(1))
|
||||
|
||||
match = PYTEST_DURATION.search(line)
|
||||
if match:
|
||||
stage.duration = float(match.group(1))
|
||||
|
||||
def run_stage(self, stage: TestStage, live: Live) -> bool:
|
||||
"""Run a single test stage and return True if passed."""
|
||||
stage.status = Status.RUNNING
|
||||
live.update(self.render_table())
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
stage.command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
cwd=self.project_root,
|
||||
env=env,
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
line = line.rstrip()
|
||||
self.parse_output(stage, line)
|
||||
live.update(self.render_table())
|
||||
|
||||
if self.verbose:
|
||||
self.console.print(line)
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
stage.status = Status.PASSED
|
||||
stage.progress = 100
|
||||
return True
|
||||
else:
|
||||
stage.status = Status.FAILED
|
||||
self.all_passed = False
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
stage.status = Status.FAILED
|
||||
stage.output.append(str(e))
|
||||
self.all_passed = False
|
||||
return False
|
||||
finally:
|
||||
live.update(self.render_table())
|
||||
|
||||
def run_all(self) -> bool:
|
||||
"""Run all test stages with fail-fast behavior."""
|
||||
self.console.print()
|
||||
|
||||
with Live(self.render_table(), console=self.console, refresh_per_second=4) as live:
|
||||
for stage in self.stages:
|
||||
if not self.run_stage(stage, live):
|
||||
# Fail fast - don't run remaining stages
|
||||
break
|
||||
|
||||
self.console.print()
|
||||
|
||||
# Print summary
|
||||
if self.all_passed:
|
||||
self.console.print("[green]All tests passed![/green]")
|
||||
else:
|
||||
self.console.print("[red]Tests failed![/red]")
|
||||
# Print failure details
|
||||
for stage in self.stages:
|
||||
if stage.status == Status.FAILED:
|
||||
self.console.print(f"\n[red]Failures in {stage.name}:[/red]")
|
||||
# Print last 20 lines of output for context
|
||||
for line in stage.output[-20:]:
|
||||
self.console.print(f" {line}")
|
||||
|
||||
return self.all_passed
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run tests with rich progress display")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Show full test output")
|
||||
parser.add_argument("--unit-only", action="store_true", help="Run only unit tests")
|
||||
parser.add_argument("--integration-only", action="store_true", help="Run only integration tests")
|
||||
args = parser.parse_args()
|
||||
|
||||
runner = TestRunner(verbose=args.verbose)
|
||||
|
||||
# Determine which stages to run
|
||||
run_unit = not args.integration_only
|
||||
run_integration = not args.unit_only
|
||||
|
||||
if run_unit:
|
||||
runner.add_stage(
|
||||
"Unit Tests",
|
||||
["uv", "run", "pytest", "tests/unit/", "-v", "--tb=short"],
|
||||
)
|
||||
|
||||
if run_integration:
|
||||
# Use the integration test script which handles containers
|
||||
runner.add_stage(
|
||||
"Integration Tests",
|
||||
["bash", "./scripts/run-integration-tests.sh"],
|
||||
)
|
||||
|
||||
success = runner.run_all()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
81
uv.lock
generated
81
uv.lock
generated
@@ -169,6 +169,8 @@ dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-httpx" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -178,7 +180,9 @@ requires-dist = [
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
||||
{ name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.32.0" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.0.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "rich", marker = "extra == 'dev'", specifier = ">=13.0.0" },
|
||||
{ name = "sse-starlette", specifier = ">=2.1.0" },
|
||||
{ name = "starlette", specifier = ">=0.41.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.32.0" },
|
||||
@@ -276,9 +280,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.23.1"
|
||||
version = "1.25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -296,9 +312,18 @@ dependencies = [
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/42/10c0c09ca27aceacd8c428956cfabdd67e3d328fe55c4abc16589285d294/mcp-1.23.1.tar.gz", hash = "sha256:7403e053e8e2283b1e6ae631423cb54736933fea70b32422152e6064556cd298", size = 596519, upload-time = "2025-12-02T18:41:12.807Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl", hash = "sha256:3ce897fcc20a41bd50b4c58d3aa88085f11f505dcc0eaed48930012d34c731d8", size = 231433, upload-time = "2025-12-02T18:41:11.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -421,7 +446,7 @@ crypto = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -430,9 +455,9 @@ dependencies = [
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -460,6 +485,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-timeout"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
@@ -471,11 +508,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
version = "0.0.21"
|
||||
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, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -527,6 +564,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.30.0"
|
||||
@@ -566,14 +616,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.0.3"
|
||||
version = "3.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306, upload-time = "2025-12-26T15:22:53.836Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442, upload-time = "2025-12-26T15:22:52.911Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -611,13 +662,13 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
version = "0.40.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user