Compare commits
No commits in common. "main" and "096711b6617a00a7d22fb5d2292160197d769efc" have entirely different histories.
main
...
096711b661
17 changed files with 3375 additions and 1450 deletions
195
.gitignore
vendored
195
.gitignore
vendored
|
@ -1,176 +1,39 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
# Rust
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# By default, Cargo will ignore "Cargo.lock" for libraries, but include for binaries
|
||||
Cargo.lock
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# Next line is for macOS Finder/Spotlight
|
||||
.DS_Store
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# VSCode/
|
||||
.vscode/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# CLion
|
||||
.idea/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
# Other common
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
*.tmp
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# dotenv, custom local env/config
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.env.*
|
||||
*.local
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# compiled output
|
||||
*.out
|
||||
*.exe
|
||||
*.bin
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
# Coverage/test/artifacts
|
||||
target/coverage/
|
||||
coverage/
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
3.13
|
2627
Cargo.lock
generated
Normal file
2627
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "trmnl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
# Only keep one of blocking+async if all features converted
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls" ] }
|
||||
dirs = "5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde_json = "1.0"
|
||||
chrono-tz = "0.8"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
scraper = "0.18"
|
||||
ical = "0.8"
|
||||
rrule = "0.14.0"
|
|
@ -1,6 +0,0 @@
|
|||
FROM python:3.13-slim-bookworm
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
RUN uv sync --frozen --no-cache
|
||||
CMD ["/app/.venv/bin/fastapi", "run", "main.py", "--port", "8887"]
|
39
Makefile
39
Makefile
|
@ -1,39 +0,0 @@
|
|||
.PHONY: help lint format lint-fix check install dev-install clean test
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " install Install dependencies"
|
||||
@echo " dev-install Install development dependencies"
|
||||
@echo " lint Run linter (check only)"
|
||||
@echo " format Format code"
|
||||
@echo " lint-fix Run linter with auto-fix"
|
||||
@echo " check Run all checks (lint + format check)"
|
||||
@echo " clean Clean cache files"
|
||||
@echo " test Run tests"
|
||||
|
||||
install:
|
||||
uv sync --no-dev
|
||||
|
||||
dev-install:
|
||||
uv sync
|
||||
|
||||
lint:
|
||||
uv run ruff check .
|
||||
|
||||
format:
|
||||
uv run ruff format .
|
||||
|
||||
lint-fix:
|
||||
uv run ruff check --fix --unsafe-fixes .
|
||||
|
||||
check: lint
|
||||
uv run ruff format --check .
|
||||
|
||||
clean:
|
||||
rm -rf .ruff_cache
|
||||
rm -rf __pycache__
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
|
||||
test:
|
||||
@echo "No tests configured yet"
|
43
README.md
43
README.md
|
@ -1,43 +0,0 @@
|
|||
# trmnl weather & pollen report
|
||||
|
||||
a custom trmnl plugin that fetches and displays weather and pollen data.
|
||||
|
||||
## setup
|
||||
|
||||
1. set up a virtual environment and install dependencies:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync
|
||||
```
|
||||
|
||||
2. set required environment variables:
|
||||
```bash
|
||||
export WEATHER_API_KEY="your_openweathermap_api_key"
|
||||
export AUTH_TOKEN="your_chosen_auth_token"
|
||||
```
|
||||
|
||||
3. run the application:
|
||||
```bash
|
||||
fastapi run main.py --port 8887
|
||||
```
|
||||
|
||||
## development
|
||||
|
||||
### install development dependencies
|
||||
|
||||
```bash
|
||||
make dev-install
|
||||
```
|
||||
|
||||
## docker
|
||||
|
||||
build and run with docker:
|
||||
```bash
|
||||
docker build -t trmnl-report .
|
||||
docker run -p 8887:8887 -e WEATHER_API_KEY=your_key -e AUTH_TOKEN=your_token trmnl-report
|
||||
```
|
||||
|
||||
## api
|
||||
|
||||
access the api at `http://localhost:8887/?token=your_auth_token`
|
536
main.py
536
main.py
|
@ -1,536 +0,0 @@
|
|||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TypedDict
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import zoneinfo
|
||||
from urllib.parse import urljoin, urlencode
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from cachetools import TTLCache
|
||||
import httpx
|
||||
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
EASTERN_TZ = zoneinfo.ZoneInfo("America/New_York")
|
||||
CACHE_TTL_SECONDS = 900 # 15 minutes
|
||||
CACHE_MAX_SIZE = 100
|
||||
POLLEN_MAX_INDEX = 12.0
|
||||
HTTP_TIMEOUT = 10.0
|
||||
DEFAULT_ZIP_CODE = "01970" # salem, ma
|
||||
DEFAULT_LATITUDE = "42.3554334"
|
||||
DEFAULT_LONGITUDE = "-71.060511"
|
||||
|
||||
|
||||
def get_required_env(key: str) -> str:
|
||||
"""Get required environment variable or raise error."""
|
||||
value = os.environ.get(key)
|
||||
if not value:
|
||||
raise ValueError(f"Required environment variable {key} is not set")
|
||||
return value
|
||||
|
||||
|
||||
class WeatherCondition(TypedDict):
|
||||
description: str
|
||||
main: str
|
||||
id: int
|
||||
icon: str
|
||||
|
||||
|
||||
class CurrentWeather(TypedDict):
|
||||
dt: int
|
||||
sunrise: int
|
||||
sunset: int
|
||||
temp: float
|
||||
feels_like: float
|
||||
pressure: int
|
||||
humidity: int
|
||||
weather: list[WeatherCondition]
|
||||
|
||||
|
||||
class DailyTemperature(TypedDict):
|
||||
min: float
|
||||
max: float
|
||||
|
||||
|
||||
class DailyWeather(TypedDict):
|
||||
dt: int
|
||||
sunrise: int
|
||||
sunset: int
|
||||
temp: DailyTemperature
|
||||
humidity: int
|
||||
pressure: int
|
||||
weather: list[WeatherCondition]
|
||||
|
||||
|
||||
class WeatherApiResponse(TypedDict):
|
||||
current: CurrentWeather
|
||||
daily: list[DailyWeather]
|
||||
|
||||
|
||||
class PollenLocation(TypedDict):
|
||||
periods: list[dict[str, str | int | float]]
|
||||
|
||||
|
||||
class PollenApiResponse(TypedDict):
|
||||
ForecastDate: str
|
||||
Location: PollenLocation
|
||||
|
||||
|
||||
class WeatherPeriod(TypedDict):
|
||||
low: int
|
||||
high: int
|
||||
desc: str
|
||||
humidity: int
|
||||
sunrise: str
|
||||
sunset: str
|
||||
pressure: int
|
||||
period: str
|
||||
|
||||
|
||||
class PollenPeriod(TypedDict):
|
||||
index: int
|
||||
period: str
|
||||
|
||||
|
||||
class WeatherReport(TypedDict):
|
||||
forecast_date: str
|
||||
current_temp: int
|
||||
current_feels_like: int
|
||||
current_humidity: int
|
||||
sunrise: str
|
||||
sunset: str
|
||||
current_pressure: int
|
||||
current_desc: str
|
||||
periods: list[WeatherPeriod]
|
||||
|
||||
|
||||
class PollenReport(TypedDict):
|
||||
forecast_date: str
|
||||
periods: list[PollenPeriod]
|
||||
|
||||
|
||||
class CurrentWeatherData(TypedDict):
|
||||
temp: int
|
||||
feels_like: int
|
||||
desc: str
|
||||
humidity: int
|
||||
pressure: int
|
||||
|
||||
|
||||
class DailyData(TypedDict, total=False):
|
||||
pollen: int
|
||||
low: int
|
||||
high: int
|
||||
desc: str
|
||||
humidity: int
|
||||
sunrise: str
|
||||
sunset: str
|
||||
pressure: int
|
||||
|
||||
|
||||
class FinalReport(TypedDict):
|
||||
fetched_at: str
|
||||
current: CurrentWeatherData
|
||||
today: DailyData
|
||||
tomorrow: DailyData
|
||||
|
||||
|
||||
app = FastAPI(title="trmnl weather & pollen report")
|
||||
|
||||
weather_cache: TTLCache = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL_SECONDS)
|
||||
pollen_cache: TTLCache = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL_SECONDS)
|
||||
|
||||
|
||||
class Config:
|
||||
"""Application configuration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
zip_code: str,
|
||||
latitude: str,
|
||||
longitude: str,
|
||||
weather_api_key: str,
|
||||
auth_token: str,
|
||||
):
|
||||
self.zip_code = zip_code
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.weather_api_key = weather_api_key
|
||||
self.auth_token = auth_token
|
||||
|
||||
|
||||
config = Config(
|
||||
zip_code=DEFAULT_ZIP_CODE,
|
||||
latitude=DEFAULT_LATITUDE,
|
||||
longitude=DEFAULT_LONGITUDE,
|
||||
weather_api_key=get_required_env("WEATHER_API_KEY"),
|
||||
auth_token=get_required_env("AUTH_TOKEN"),
|
||||
)
|
||||
|
||||
|
||||
class DateTimeFormatter:
|
||||
"""Utility class for datetime formatting."""
|
||||
|
||||
@staticmethod
|
||||
def format_date(dt: datetime) -> str:
|
||||
"""Format datetime as abbreviated date string (e.g., 'mon 15')."""
|
||||
dt = dt.astimezone(EASTERN_TZ)
|
||||
return dt.strftime("%a %d").lower()
|
||||
|
||||
@staticmethod
|
||||
def format_time(dt: datetime) -> str:
|
||||
"""Format datetime as time string (e.g., '02:30pm')."""
|
||||
dt = dt.astimezone(EASTERN_TZ)
|
||||
return dt.strftime("%I:%M%p").lower()
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Format datetime as date and time string."""
|
||||
return f"{DateTimeFormatter.format_date(dt)} {DateTimeFormatter.format_time(dt)}"
|
||||
|
||||
@staticmethod
|
||||
def relative_day_to_date(relative_day: str) -> str:
|
||||
"""Convert relative day string to formatted date string."""
|
||||
now = datetime.now()
|
||||
day_delta = timedelta(days=1)
|
||||
|
||||
relative_day = relative_day.lower().strip()
|
||||
|
||||
if relative_day == "yesterday":
|
||||
return DateTimeFormatter.format_date(now - day_delta)
|
||||
elif relative_day == "today":
|
||||
return DateTimeFormatter.format_date(now)
|
||||
elif relative_day == "tomorrow":
|
||||
return DateTimeFormatter.format_date(now + day_delta)
|
||||
else:
|
||||
return relative_day
|
||||
|
||||
|
||||
class WeatherData:
|
||||
"""Data model for weather information."""
|
||||
|
||||
def __init__(self, raw_data: WeatherApiResponse):
|
||||
self.raw_data = raw_data
|
||||
self._validate_data()
|
||||
|
||||
def _validate_data(self) -> None:
|
||||
"""Validate required fields in weather data."""
|
||||
required_fields = ["current", "daily"]
|
||||
for field in required_fields:
|
||||
if field not in self.raw_data:
|
||||
raise ValueError(f"Missing required field in weather data: {field}")
|
||||
|
||||
@property
|
||||
def current(self) -> CurrentWeather:
|
||||
"""Get current weather data."""
|
||||
return self.raw_data["current"]
|
||||
|
||||
@property
|
||||
def daily_periods(self) -> list[DailyWeather]:
|
||||
"""Get daily forecast periods (limited to next 2 days)."""
|
||||
return self.raw_data["daily"][:2]
|
||||
|
||||
|
||||
class PollenData:
|
||||
"""Data model for pollen information."""
|
||||
|
||||
def __init__(self, raw_data: PollenApiResponse):
|
||||
self.raw_data = raw_data
|
||||
self._validate_data()
|
||||
|
||||
def _validate_data(self) -> None:
|
||||
"""Validate required fields in pollen data."""
|
||||
required_fields = ["ForecastDate", "Location"]
|
||||
for field in required_fields:
|
||||
if field not in self.raw_data:
|
||||
raise ValueError(f"Missing required field in pollen data: {field}")
|
||||
|
||||
@property
|
||||
def forecast_date(self) -> str:
|
||||
"""Get formatted forecast date."""
|
||||
dt = datetime.fromisoformat(self.raw_data["ForecastDate"])
|
||||
return DateTimeFormatter.format_datetime(dt)
|
||||
|
||||
@property
|
||||
def periods(self) -> list[PollenPeriod]:
|
||||
"""Get pollen periods for today and tomorrow."""
|
||||
periods = self.raw_data["Location"].get("periods", [])
|
||||
valid_periods: list[PollenPeriod] = []
|
||||
|
||||
for period in periods:
|
||||
period_type = str(period.get("Type", "")).lower().strip()
|
||||
if period_type in ["today", "tomorrow"]:
|
||||
index_value = float(period.get("Index", 0))
|
||||
# convert pollen index to percentage scale
|
||||
pollen_percentage = int(index_value / POLLEN_MAX_INDEX * 100)
|
||||
|
||||
valid_periods.append(
|
||||
PollenPeriod(
|
||||
{
|
||||
"index": pollen_percentage,
|
||||
"period": DateTimeFormatter.relative_day_to_date(period_type),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return valid_periods
|
||||
|
||||
|
||||
def error_handler(func):
|
||||
"""Decorator to handle exceptions in async functions."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in {func.__name__}: {e}")
|
||||
return []
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class WeatherService:
|
||||
"""Service for fetching weather data."""
|
||||
|
||||
@staticmethod
|
||||
@error_handler
|
||||
async def fetch_weather(latitude: str, longitude: str, api_key: str) -> list[WeatherReport]:
|
||||
"""Fetch weather data from OpenWeatherMap API."""
|
||||
cache_key = (latitude, longitude)
|
||||
|
||||
if cache_key in weather_cache:
|
||||
return weather_cache[cache_key]
|
||||
|
||||
base_url = "https://api.openweathermap.org/data/3.0/onecall"
|
||||
params = {
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
"appid": api_key,
|
||||
"units": "imperial",
|
||||
}
|
||||
url = f"{base_url}?{urlencode(params)}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
raw_data = response.json()
|
||||
|
||||
weather_data = WeatherData(raw_data)
|
||||
result = WeatherService._format_weather_data(weather_data)
|
||||
|
||||
weather_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _format_weather_data(weather_data: WeatherData) -> list[WeatherReport]:
|
||||
"""Format weather data for API response."""
|
||||
current = weather_data.current
|
||||
daily_periods = weather_data.daily_periods
|
||||
|
||||
current_dt = datetime.fromtimestamp(current["dt"])
|
||||
|
||||
return [
|
||||
WeatherReport(
|
||||
{
|
||||
"forecast_date": DateTimeFormatter.format_datetime(current_dt),
|
||||
"current_temp": int(round(current["temp"])),
|
||||
"current_feels_like": int(round(current["feels_like"])),
|
||||
"current_humidity": current["humidity"],
|
||||
"sunrise": DateTimeFormatter.format_time(
|
||||
datetime.fromtimestamp(current["sunrise"])
|
||||
),
|
||||
"sunset": DateTimeFormatter.format_time(
|
||||
datetime.fromtimestamp(current["sunset"])
|
||||
),
|
||||
"current_pressure": current["pressure"],
|
||||
"current_desc": current["weather"][0]["description"],
|
||||
"periods": [
|
||||
WeatherService._format_daily_period(period) for period in daily_periods
|
||||
],
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _format_daily_period(period: DailyWeather) -> WeatherPeriod:
|
||||
"""Format a single daily weather period."""
|
||||
period_dt = datetime.fromtimestamp(period["dt"])
|
||||
|
||||
return WeatherPeriod(
|
||||
{
|
||||
"low": int(round(period["temp"]["min"])),
|
||||
"high": int(round(period["temp"]["max"])),
|
||||
"desc": period["weather"][0]["description"],
|
||||
"humidity": period["humidity"],
|
||||
"sunrise": DateTimeFormatter.format_time(datetime.fromtimestamp(period["sunrise"])),
|
||||
"sunset": DateTimeFormatter.format_time(datetime.fromtimestamp(period["sunset"])),
|
||||
"pressure": period["pressure"],
|
||||
"period": DateTimeFormatter.format_date(period_dt),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class PollenService:
|
||||
"""Service for fetching pollen data."""
|
||||
|
||||
@staticmethod
|
||||
@error_handler
|
||||
async def fetch_pollen(zip_code: str) -> list[PollenReport]:
|
||||
"""Fetch pollen data from pollen.com API."""
|
||||
if zip_code in pollen_cache:
|
||||
return pollen_cache[zip_code]
|
||||
|
||||
base_url = "https://www.pollen.com/api/forecast/current/pollen/"
|
||||
url = urljoin(base_url, zip_code)
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/123.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": url,
|
||||
"Cookie": f"geo={zip_code}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
raw_data = response.json()
|
||||
|
||||
pollen_data = PollenData(raw_data)
|
||||
result = [
|
||||
PollenReport(
|
||||
{
|
||||
"forecast_date": pollen_data.forecast_date,
|
||||
"periods": pollen_data.periods,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
pollen_cache[zip_code] = result
|
||||
return result
|
||||
|
||||
|
||||
class DataAggregator:
|
||||
"""Service for aggregating weather and pollen data."""
|
||||
|
||||
@staticmethod
|
||||
def build_daily_data(
|
||||
date: str,
|
||||
pollen_periods: dict[str, PollenPeriod],
|
||||
weather_periods: dict[str, WeatherPeriod],
|
||||
) -> DailyData:
|
||||
"""Build daily data combining weather and pollen information."""
|
||||
daily_data: DailyData = {}
|
||||
|
||||
if date in pollen_periods:
|
||||
daily_data["pollen"] = pollen_periods[date]["index"]
|
||||
|
||||
if date in weather_periods:
|
||||
weather_data = weather_periods[date]
|
||||
daily_data.update(
|
||||
{
|
||||
"low": weather_data["low"],
|
||||
"high": weather_data["high"],
|
||||
"desc": weather_data["desc"],
|
||||
"humidity": weather_data["humidity"],
|
||||
"sunrise": weather_data["sunrise"],
|
||||
"sunset": weather_data["sunset"],
|
||||
"pressure": weather_data["pressure"],
|
||||
}
|
||||
)
|
||||
|
||||
return daily_data
|
||||
|
||||
@staticmethod
|
||||
def create_periods_lookup(
|
||||
pollen_data: list[PollenReport], weather_data: list[WeatherReport]
|
||||
) -> tuple[dict[str, PollenPeriod], dict[str, WeatherPeriod], CurrentWeatherData]:
|
||||
"""Create lookup dictionaries for pollen and weather periods."""
|
||||
pollen_periods: dict[str, PollenPeriod] = {}
|
||||
weather_periods: dict[str, WeatherPeriod] = {}
|
||||
|
||||
if pollen_data and pollen_data[0].get("periods"):
|
||||
pollen_periods = {p["period"]: p for p in pollen_data[0]["periods"]}
|
||||
|
||||
if weather_data and weather_data[0].get("periods"):
|
||||
weather_periods = {p["period"]: p for p in weather_data[0]["periods"]}
|
||||
|
||||
current_weather_info = CurrentWeatherData(
|
||||
{
|
||||
"temp": 0,
|
||||
"feels_like": 0,
|
||||
"desc": "",
|
||||
"humidity": 0,
|
||||
"pressure": 0,
|
||||
}
|
||||
)
|
||||
|
||||
if weather_data and weather_data[0]:
|
||||
data = weather_data[0]
|
||||
current_weather_info = CurrentWeatherData(
|
||||
{
|
||||
"temp": data["current_temp"],
|
||||
"feels_like": data["current_feels_like"],
|
||||
"desc": data["current_desc"],
|
||||
"humidity": data["current_humidity"],
|
||||
"pressure": data["current_pressure"],
|
||||
}
|
||||
)
|
||||
|
||||
return pollen_periods, weather_periods, current_weather_info
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def get_weather_pollen_report(token: str) -> FinalReport:
|
||||
"""
|
||||
Get weather and pollen report.
|
||||
|
||||
Args:
|
||||
token: Authentication token
|
||||
|
||||
Returns:
|
||||
Dictionary containing current, today, and tomorrow weather/pollen data
|
||||
|
||||
Raises:
|
||||
HTTPException: If authentication fails
|
||||
"""
|
||||
if token != config.auth_token:
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
pollen_data, weather_data = await asyncio.gather(
|
||||
PollenService.fetch_pollen(config.zip_code),
|
||||
WeatherService.fetch_weather(config.latitude, config.longitude, config.weather_api_key),
|
||||
)
|
||||
|
||||
pollen_periods, weather_periods, current_weather_info = DataAggregator.create_periods_lookup(
|
||||
pollen_data, weather_data
|
||||
)
|
||||
|
||||
now = datetime.now()
|
||||
today_date = DateTimeFormatter.format_date(now)
|
||||
tomorrow_date = DateTimeFormatter.format_date(now + timedelta(days=1))
|
||||
|
||||
result: FinalReport = FinalReport(
|
||||
{
|
||||
"fetched_at": DateTimeFormatter.format_datetime(now),
|
||||
"current": current_weather_info,
|
||||
"today": DailyData({}),
|
||||
"tomorrow": DailyData({}),
|
||||
}
|
||||
)
|
||||
|
||||
today_data = DataAggregator.build_daily_data(today_date, pollen_periods, weather_periods)
|
||||
if today_data:
|
||||
result["today"] = today_data
|
||||
|
||||
tomorrow_data = DataAggregator.build_daily_data(tomorrow_date, pollen_periods, weather_periods)
|
||||
if tomorrow_data:
|
||||
result["tomorrow"] = tomorrow_data
|
||||
|
||||
return result
|
|
@ -1,23 +0,0 @@
|
|||
[project]
|
||||
name = "trmnl-report"
|
||||
version = "0.1.0"
|
||||
description = "Custom TRMNL plugin"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.115.12",
|
||||
"httpx>=0.28.1",
|
||||
"cachetools>=5.0.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
line-length = 100
|
||||
indent-width = 4
|
||||
|
||||
[tool.pyright]
|
||||
venvPath = "."
|
||||
venv = ".venv"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["ruff>=0.12.11"]
|
52
src/cache.rs
Normal file
52
src/cache.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Cache<T> {
|
||||
pub timestamp: u64,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
fn get_cache_path(report_type: &str) -> PathBuf {
|
||||
let home = std::env::var("HOME").expect("HOME not set");
|
||||
let cache_dir = PathBuf::from(format!("{}/.local/state/trmnl", home));
|
||||
let _ = fs::create_dir_all(&cache_dir);
|
||||
cache_dir.join(format!("{}.json", report_type))
|
||||
}
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
pub fn load_cache<T: Serialize + DeserializeOwned>(
|
||||
report_type: &str,
|
||||
expiry_secs: u64,
|
||||
) -> Option<T> {
|
||||
let path = get_cache_path(report_type);
|
||||
let contents = fs::read_to_string(&path).ok()?;
|
||||
let parsed: Cache<T> = serde_json::from_str(&contents).ok()?;
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
if now - parsed.timestamp <= expiry_secs {
|
||||
println!("(using cached {} report)", report_type);
|
||||
Some(parsed.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_cache<T: Serialize>(report_type: &str, data: &T) {
|
||||
let path = get_cache_path(report_type);
|
||||
let to_save = Cache {
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
data: data,
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&to_save).unwrap();
|
||||
let _ = fs::write(path, json);
|
||||
}
|
188
src/calendar.rs
Normal file
188
src/calendar.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use chrono::{DateTime, FixedOffset, NaiveDate, Offset, TimeZone, Utc};
|
||||
use ical::IcalParser;
|
||||
use rrule::{RRule, RRuleSet, Unvalidated};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CalendarEventSummary {
|
||||
pub start: Option<DateTime<FixedOffset>>,
|
||||
pub summary: String,
|
||||
pub all_day: bool,
|
||||
}
|
||||
|
||||
pub async fn fetch_next_events(
|
||||
calendar_url: &str,
|
||||
max_events: usize,
|
||||
tz_str: &str,
|
||||
) -> Result<Vec<CalendarEventSummary>, Box<dyn Error>> {
|
||||
let resp = reqwest::get(calendar_url).await?;
|
||||
let body = resp.bytes().await?;
|
||||
let ical_str = String::from_utf8_lossy(&body);
|
||||
|
||||
let parser = IcalParser::new(ical_str.as_bytes());
|
||||
let mut events: Vec<CalendarEventSummary> = Vec::new();
|
||||
|
||||
let tz: Option<chrono_tz::Tz> = tz_str.parse().ok();
|
||||
for calendar in parser {
|
||||
for evt in calendar?.events {
|
||||
let mut summary = None;
|
||||
let mut dtstart = None;
|
||||
let mut all_day = false;
|
||||
let mut rrule_str: Option<String> = None;
|
||||
let mut raw_dtstart: Option<String> = None;
|
||||
let mut dt_params: Option<Vec<(String, Vec<String>)>> = None;
|
||||
for prop in &evt.properties {
|
||||
match prop.name.as_str() {
|
||||
"SUMMARY" => summary = prop.value.clone(),
|
||||
"DTSTART" => {
|
||||
raw_dtstart = prop.value.clone();
|
||||
dt_params = prop.params.clone();
|
||||
if let Some(val) = &prop.value {
|
||||
// -------- Existing DTSTART parsing logic goes below ------
|
||||
// All-day check
|
||||
let is_all_day = prop
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.iter().find(|(k, _)| k == "VALUE"))
|
||||
.and_then(|(_, v)| v.first())
|
||||
.is_some_and(|v| v == "DATE");
|
||||
|
||||
if is_all_day {
|
||||
if let Ok(date) = NaiveDate::parse_from_str(val, "%Y%m%d") {
|
||||
if let Some(tz) = tz {
|
||||
if let Some(dt) = tz
|
||||
.from_local_datetime(
|
||||
&date.and_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.single()
|
||||
{
|
||||
dtstart = Some(dt.with_timezone(&dt.offset().fix()));
|
||||
}
|
||||
}
|
||||
all_day = true;
|
||||
}
|
||||
} else if let Some(params) = &prop.params {
|
||||
// Check and handle TZID param!
|
||||
if let Some((_, tz_vec)) = params.iter().find(|(k, _)| k == "TZID")
|
||||
{
|
||||
let tz_id = &tz_vec[0];
|
||||
if let Ok(parsed_tz) = tz_id.parse::<chrono_tz::Tz>() {
|
||||
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(
|
||||
val,
|
||||
"%Y%m%dT%H%M%S",
|
||||
) {
|
||||
let local_dt =
|
||||
parsed_tz.from_local_datetime(&naive_dt).single();
|
||||
if let Some(dt) = local_dt {
|
||||
dtstart =
|
||||
Some(dt.with_timezone(&dt.offset().fix()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(dt) = DateTime::parse_from_rfc3339(val) {
|
||||
dtstart = Some(dt.with_timezone(dt.offset()));
|
||||
} else if let Ok(dt) =
|
||||
DateTime::parse_from_str(val, "%Y%m%dT%H%M%SZ")
|
||||
{
|
||||
dtstart = Some(dt.with_timezone(&Utc.fix()));
|
||||
}
|
||||
} else if val.ends_with('Z') && val.len() == 16 {
|
||||
// e.g. "20250522T181500Z" not RFC3339, convert to RFC3339
|
||||
let iso = format!(
|
||||
"{}-{}-{}T{}:{}:{}Z",
|
||||
&val[0..4],
|
||||
&val[4..6],
|
||||
&val[6..8],
|
||||
&val[9..11],
|
||||
&val[11..13],
|
||||
&val[13..15]
|
||||
);
|
||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&iso) {
|
||||
dtstart = Some(dt.with_timezone(&Utc.fix()));
|
||||
}
|
||||
} else if let Ok(dt) = DateTime::parse_from_rfc3339(val) {
|
||||
dtstart = Some(dt.with_timezone(dt.offset()));
|
||||
} else if let Ok(dt) = DateTime::parse_from_str(val, "%Y%m%dT%H%M%S") {
|
||||
// No Z/zone, treat as in configured tz
|
||||
if let Some(tz) = tz {
|
||||
if let Some(dt2) =
|
||||
tz.from_local_datetime(&dt.naive_local()).single()
|
||||
{
|
||||
dtstart = Some(dt2.with_timezone(&dt2.offset().fix()));
|
||||
}
|
||||
}
|
||||
} else if let Ok(date) = NaiveDate::parse_from_str(val, "%Y%m%d") {
|
||||
// As a fallback, treat as all-day
|
||||
if let Some(tz) = tz {
|
||||
if let Some(dt2) = tz
|
||||
.from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
|
||||
.single()
|
||||
{
|
||||
dtstart = Some(dt2.with_timezone(&dt2.offset().fix()));
|
||||
}
|
||||
}
|
||||
all_day = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
"RRULE" => rrule_str = prop.value.clone(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// ----------- RRULE recurring event expansion block -----------
|
||||
if let (Some(ref s), Some(dt), Some(ref rrule_val), Some(_val)) = (
|
||||
summary.clone(),
|
||||
dtstart,
|
||||
rrule_str.as_ref(),
|
||||
raw_dtstart.as_ref(),
|
||||
) {
|
||||
// dtstart is FixedOffset, convert to Utc for rrule
|
||||
let dtstart_rrule = dt.with_timezone(&rrule::Tz::UTC);
|
||||
if let Ok(unvalid) = rrule_val.parse::<RRule<Unvalidated>>() {
|
||||
if let Ok(rrule) = unvalid.validate(dtstart_rrule) {
|
||||
let set = RRuleSet::new(dtstart_rrule).rrule(rrule);
|
||||
// Expand up to the next 20 future instances for each recurring event
|
||||
let now = Utc::now();
|
||||
let instances = set.all(1000);
|
||||
let occur_iter = instances
|
||||
.dates
|
||||
.iter()
|
||||
.filter(|t| **t > now)
|
||||
.take(max_events);
|
||||
for occ in occur_iter {
|
||||
let occ_fixed: DateTime<FixedOffset> =
|
||||
occ.with_timezone(&dt.offset().fix());
|
||||
events.push(CalendarEventSummary {
|
||||
start: Some(occ_fixed),
|
||||
summary: s.clone(),
|
||||
all_day,
|
||||
});
|
||||
}
|
||||
} else if dtstart_rrule > Utc::now() {
|
||||
eprintln!("[ERROR] Failed to validate RRULE: {:?}", rrule_val);
|
||||
}
|
||||
// Otherwise, ignore and continue
|
||||
} else {
|
||||
eprintln!("[ERROR] Failed to parse RRULE: {:?}", rrule_val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-recurring event
|
||||
if let (Some(s), Some(dt)) = (summary.clone(), dtstart) {
|
||||
events.push(CalendarEventSummary {
|
||||
start: Some(dt),
|
||||
summary: s,
|
||||
all_day,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter to only future events
|
||||
let now = Utc::now();
|
||||
events.retain(|e| e.start.map(|s| s > now).unwrap_or(false));
|
||||
// Sort by time ascending, then take first max_events
|
||||
events.sort_by_key(|e| e.start);
|
||||
Ok(events.into_iter().take(max_events).collect())
|
||||
}
|
40
src/config.rs
Normal file
40
src/config.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use dirs::config_dir;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub cache_weather_secs: Option<u64>,
|
||||
pub cache_pollen_secs: Option<u64>,
|
||||
pub cache_calendar_secs: Option<u64>,
|
||||
pub weather_api_key: String,
|
||||
pub calendar_event_count: usize,
|
||||
pub location: String,
|
||||
pub timezone: String,
|
||||
pub pollen_zip: String,
|
||||
pub calendar_url: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// Try primary platform config_dir first (macOS: ~/Library/Application Support/trmnl/config.toml, Linux: ~/.config/trmnl/config.toml)
|
||||
let mut config_path = config_dir().ok_or("Could not find user config directory")?;
|
||||
config_path.push("trmnl/config.toml");
|
||||
if config_path.exists() {
|
||||
let contents = fs::read_to_string(&config_path)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
return Ok(config);
|
||||
}
|
||||
// Fallback: try ~/.config/trmnl/config.toml explicitly
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
let fallback_path: PathBuf = home_dir.join(".config/trmnl/config.toml");
|
||||
if fallback_path.exists() {
|
||||
let contents = fs::read_to_string(&fallback_path)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
return Ok(config);
|
||||
}
|
||||
}
|
||||
Err("Config file not found in either location".into())
|
||||
}
|
||||
}
|
199
src/main.rs
Normal file
199
src/main.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
mod cache;
|
||||
mod calendar;
|
||||
mod config;
|
||||
mod pollen;
|
||||
mod weather;
|
||||
|
||||
use calendar::fetch_next_events;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use config::Config;
|
||||
use pollen::fetch_pollen_api;
|
||||
use weather::{fetch_weather, geocode_city, WeatherSummary};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = match Config::load() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to load config: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Weather - with cache
|
||||
let cache_weather_secs = config.cache_weather_secs.unwrap_or(300);
|
||||
let weather: Option<WeatherSummary> =
|
||||
if let Some(w) = cache::load_cache::<WeatherSummary>("weather", cache_weather_secs) {
|
||||
Some(w)
|
||||
} else {
|
||||
match fetch_weather(&config.location, &config.weather_api_key).await {
|
||||
Ok(data) => {
|
||||
cache::save_cache("weather", &data);
|
||||
Some(data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch weather: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
let (_lat, _lon) = if weather.is_some() {
|
||||
match geocode_city(&config.location, &config.weather_api_key).await {
|
||||
Ok((lat, lon)) => (lat, lon),
|
||||
Err(_) => (0.0, 0.0),
|
||||
}
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
if let Some(w) = &weather {
|
||||
if w.obs_time_unix > 0 {
|
||||
match config.timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let dt_local = tz.from_utc_datetime(
|
||||
&Utc.timestamp_opt(w.obs_time_unix, 0).unwrap().naive_utc(),
|
||||
);
|
||||
println!(
|
||||
"Weather (as of {}):",
|
||||
dt_local.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(
|
||||
"Weather (as of unix timestamp {}) (timezone parse error)",
|
||||
w.obs_time_unix
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Weather:");
|
||||
}
|
||||
println!(" - Temperature: {}", w.temp);
|
||||
println!(" - Conditions: {}", w.current_desc);
|
||||
println!(" - High: {}, Low: {}", w.high, w.low);
|
||||
println!(" - Forecast: {}", w.daily_desc);
|
||||
} else {
|
||||
println!("Weather: N/A");
|
||||
}
|
||||
|
||||
// Pollen - with cache
|
||||
let cache_pollen_secs = config.cache_pollen_secs.unwrap_or(300);
|
||||
match if let Some(p) = cache::load_cache::<pollen::PollenSummary>("pollen", cache_pollen_secs) {
|
||||
Ok(p)
|
||||
} else {
|
||||
match fetch_pollen_api(&config.pollen_zip).await {
|
||||
Ok(data) => {
|
||||
cache::save_cache("pollen", &data);
|
||||
Ok(data)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} {
|
||||
Ok(p) => {
|
||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&p.forecast_date) {
|
||||
match config.timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let local_time = dt.with_timezone(&tz);
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {}:",
|
||||
p.location,
|
||||
local_time.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {} (timezone parse error):",
|
||||
p.location, p.forecast_date
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {}:",
|
||||
p.location, p.forecast_date
|
||||
);
|
||||
}
|
||||
|
||||
for day in &["Yesterday", "Today", "Tomorrow"] {
|
||||
if let Some(period) = p.periods.get(*day) {
|
||||
println!(
|
||||
" {:9}: {:>4.1} ({})",
|
||||
day,
|
||||
period.index,
|
||||
period.triggers.join(", ")
|
||||
);
|
||||
} else {
|
||||
println!(" {:9}: N/A", day);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("\nFailed to fetch pollen (API): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar - with cache
|
||||
let cache_calendar_secs = config.cache_calendar_secs.unwrap_or(300);
|
||||
println!("\nUpcoming calendar events:");
|
||||
|
||||
match if let Some(events) =
|
||||
cache::load_cache::<Vec<calendar::CalendarEventSummary>>("calendar", cache_calendar_secs)
|
||||
{
|
||||
Ok(events)
|
||||
} else {
|
||||
match fetch_next_events(
|
||||
&config.calendar_url,
|
||||
config.calendar_event_count,
|
||||
&config.timezone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => {
|
||||
cache::save_cache("calendar", &data);
|
||||
Ok(data)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} {
|
||||
Ok(events) if !events.is_empty() => {
|
||||
for (i, event) in events.iter().enumerate() {
|
||||
let day = event
|
||||
.start
|
||||
.map(|dt| dt.format("%a %Y-%m-%d").to_string())
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let time = if event.all_day {
|
||||
"all day".to_string()
|
||||
} else if let Some(dt) = event.start {
|
||||
dt.format("%H:%M").to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
println!("{}. {} {:>8} {}", i + 1, day, time, event.summary);
|
||||
}
|
||||
}
|
||||
_ => println!("No upcoming calendar events found."),
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("MBTA Transit (placeholder):");
|
||||
println!(" - Red Line: Ashmont in 5 min, Braintree in 10 min");
|
||||
println!(" - Orange Line: Northbound in 7 min");
|
||||
println!(" - Bus 64: Due in 2 min");
|
||||
// In the future, fetch real data from an MBTA API here.
|
||||
|
||||
println!("Inbound Shopify packages:");
|
||||
let packages = get_shopify_packages(&config);
|
||||
for (i, pkg) in packages.iter().enumerate() {
|
||||
println!("{}. {}", i + 1, pkg);
|
||||
}
|
||||
}
|
||||
|
||||
// No longer needed (handled above)
|
||||
// fn get_calendar_events(_config: &Config, n: usize) -> Vec<String> { ... }
|
||||
|
||||
fn get_shopify_packages(_config: &Config) -> Vec<String> {
|
||||
vec![
|
||||
"Order #1234: Shipped - Arriving May 13".to_string(),
|
||||
"Order #5678: In transit - Arriving May 15".to_string(),
|
||||
]
|
||||
}
|
81
src/pollen.rs
Normal file
81
src/pollen.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct PollenApiResponse {
|
||||
forecast_date: String,
|
||||
location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Location {
|
||||
periods: Vec<Period>,
|
||||
#[serde(rename = "DisplayLocation")]
|
||||
display_location: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct Period {
|
||||
r#type: String,
|
||||
index: f32,
|
||||
triggers: Vec<Trigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct Trigger {
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
plant_type: String,
|
||||
#[allow(dead_code)]
|
||||
genus: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PollenPeriod {
|
||||
pub index: f32,
|
||||
pub triggers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PollenSummary {
|
||||
pub location: String,
|
||||
pub forecast_date: String,
|
||||
pub periods: HashMap<String, PollenPeriod>, // "Yesterday", "Today", "Tomorrow"
|
||||
}
|
||||
|
||||
pub async fn fetch_pollen_api(zip: &str) -> Result<PollenSummary, Box<dyn Error>> {
|
||||
let url = format!("https://www.pollen.com/api/forecast/current/pollen/{}", zip);
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("Referer", &url)
|
||||
.header("Cookie", format!("geo={}", zip))
|
||||
.send().await?
|
||||
.text().await?;
|
||||
|
||||
let api: PollenApiResponse = serde_json::from_str(&resp)?;
|
||||
let loc = api.location.ok_or("No location in pollen.com response")?;
|
||||
let mut periods = HashMap::new();
|
||||
|
||||
for period in &loc.periods {
|
||||
let triggers: Vec<String> = period.triggers.iter().map(|t| t.name.clone()).collect();
|
||||
periods.insert(
|
||||
period.r#type.clone(),
|
||||
PollenPeriod {
|
||||
index: period.index,
|
||||
triggers,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(PollenSummary {
|
||||
location: loc.display_location,
|
||||
forecast_date: api.forecast_date,
|
||||
periods,
|
||||
})
|
||||
}
|
141
src/weather.rs
Normal file
141
src/weather.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeocodeResponse {
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
#[allow(dead_code)]
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
country: String,
|
||||
}
|
||||
|
||||
// returns (lat, lon) for specified city name
|
||||
pub async fn geocode_city(city: &str, api_key: &str) -> Result<(f64, f64), Box<dyn Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let geocode_url = format!(
|
||||
"https://api.openweathermap.org/geo/1.0/direct?q={}&limit=1&appid={}",
|
||||
city, api_key
|
||||
);
|
||||
let geo_resp = client.get(&geocode_url).send().await?;
|
||||
let geo_text = geo_resp.text().await?;
|
||||
let geo_parsed: Result<Vec<GeocodeResponse>, serde_json::Error> =
|
||||
serde_json::from_str(&geo_text);
|
||||
match geo_parsed {
|
||||
Ok(geo_vec) => {
|
||||
if geo_vec.is_empty() {
|
||||
return Err(format!(
|
||||
"No geocoding result for city: {} (response: {})",
|
||||
city, geo_text
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok((geo_vec[0].lat, geo_vec[0].lon))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode geocoding response. Raw response: {geo_text}");
|
||||
Err(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
// ------------------ WEATHER -------------------------
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherCurrent {
|
||||
pub temp: f64,
|
||||
pub weather: Vec<WeatherDesc>,
|
||||
pub dt: i64, // Unix timestamp
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDailyTemp {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDesc {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDaily {
|
||||
pub temp: WeatherDailyTemp,
|
||||
pub weather: Vec<WeatherDesc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OneCallResult {
|
||||
pub current: WeatherCurrent,
|
||||
pub daily: Vec<WeatherDaily>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WeatherSummary {
|
||||
pub temp: String,
|
||||
pub current_desc: String,
|
||||
pub high: String,
|
||||
pub low: String,
|
||||
pub daily_desc: String,
|
||||
pub obs_time_unix: i64,
|
||||
}
|
||||
|
||||
/// Returns WeatherSummary struct
|
||||
pub async fn fetch_weather(city: &str, api_key: &str) -> Result<WeatherSummary, Box<dyn Error>> {
|
||||
let (lat, lon) = geocode_city(city, api_key).await?;
|
||||
let client = reqwest::Client::new();
|
||||
// Get weather data from One Call 3.0
|
||||
let onecall_url = format!(
|
||||
"https://api.openweathermap.org/data/3.0/onecall?lat={}&lon={}&appid={}&units=imperial",
|
||||
lat, lon, api_key
|
||||
);
|
||||
let one_resp = client.get(&onecall_url).send().await?;
|
||||
let one_status = one_resp.status();
|
||||
let one_text = one_resp.text().await?;
|
||||
if !one_status.is_success() {
|
||||
return Err(format!("HTTP error {}: {}", one_status, one_text).into());
|
||||
}
|
||||
|
||||
let one_parsed: OneCallResult = match serde_json::from_str(&one_text) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode One Call weather response. Raw response: {one_text}");
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Current conditions
|
||||
let temp = format!("{:.1}°F", one_parsed.current.temp);
|
||||
let current_desc = one_parsed
|
||||
.current
|
||||
.weather
|
||||
.first()
|
||||
.map(|w| w.description.clone())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
let current_dt = one_parsed.current.dt; // UNIX timestamp, UTC
|
||||
|
||||
// Today's forecast is daily[0]
|
||||
let (high, low, daily_desc) = if let Some(today) = one_parsed.daily.first() {
|
||||
let high = format!("{:.1}°F", today.temp.max);
|
||||
let low = format!("{:.1}°F", today.temp.min);
|
||||
let desc = today
|
||||
.weather
|
||||
.first()
|
||||
.map(|w| w.description.clone())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
(high, low, desc)
|
||||
} else {
|
||||
("N/A".to_string(), "N/A".to_string(), "N/A".to_string())
|
||||
};
|
||||
|
||||
Ok(WeatherSummary {
|
||||
temp,
|
||||
current_desc,
|
||||
high,
|
||||
low,
|
||||
daily_desc,
|
||||
obs_time_unix: current_dt,
|
||||
})
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
<div>
|
||||
<div class="flex flex--row">
|
||||
<div>
|
||||
<span class="value">{{ current.temp }}°F</span>
|
||||
<span class="label text--gray-4">feels like {{ current.feels_like }}°F</span>
|
||||
</div>
|
||||
<div class="stretch text--right">
|
||||
<span class="value">{{ current.desc }}</span>
|
||||
<span class="label text--gray-4">conditions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex--row">
|
||||
<div>
|
||||
<span class="value">{{ current.humidity }}%</span>
|
||||
<span class="label text--gray-4">humidity</span>
|
||||
</div>
|
||||
<div class="stretch text--center">
|
||||
<span class="value">{{ current.pressure }}</span>
|
||||
<span class="label text--gray-4">pressure</span>
|
||||
</div>
|
||||
<div class="text--right">
|
||||
<span class="value">{{ fetched_at }}</span>
|
||||
<span class="label text--gray-4">date</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border--h-1 pt--4"></div>
|
||||
|
||||
<div class="pt--4">
|
||||
<div class="flex flex--row">
|
||||
{% assign periods = "today,tomorrow" | split: "," %}
|
||||
{% for period in periods %}
|
||||
{% assign data = [period] %}
|
||||
<div class="stretch ">
|
||||
<div>
|
||||
<div class="label label--inverted text--center">{{ period }}</div>
|
||||
|
||||
<div class="pt--2 text--center">
|
||||
<span class="value">
|
||||
{{ data.low }}°F / {{ data.high }}°F
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text--center">
|
||||
<span class="value">{{ data.desc }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text--center">
|
||||
<span class="value">{{ data.sunrise }} - {{ data.sunset }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex--row">
|
||||
<div class="text--center">
|
||||
<span class="value">{{ data.humidity }}%</span>
|
||||
<span class="label text--gray-4">humidity</span>
|
||||
</div>
|
||||
<div class="stretch text--center">
|
||||
<span class="value">{{ data.pressure }}</span>
|
||||
<span class="label text--gray-4">pressure</span>
|
||||
</div>
|
||||
<div class="text--center">
|
||||
<span class="value">{{ data.pollen }}%</span>
|
||||
<span class="label text--gray-4">pollen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
563
uv.lock
generated
563
uv.lock
generated
|
@ -1,563 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
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, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "email-validator" },
|
||||
{ name = "fastapi-cli", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-cli"
|
||||
version = "0.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "rich-toolkit" },
|
||||
{ name = "typer" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753, upload-time = "2024-12-15T14:28:10.028Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705, upload-time = "2024-12-15T14:28:06.18Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.6.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
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, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ 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, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pydantic"
|
||||
version = "2.11.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/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
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, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ 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, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich-toolkit"
|
||||
version = "0.14.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/31/b6d055f291a660a7bcaec4bcc9457b9fef8ecb6293e527b1eef1840aefd4/rich_toolkit-0.14.6.tar.gz", hash = "sha256:9dbd40e83414b84e828bf899115fff8877ce5951b73175f44db142902f07645d", size = 110805, upload-time = "2025-05-12T19:19:15.284Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/3c/7a824c0514e87c61000583ac22c8321da6dc8e58a93d5f56e583482a2ee0/rich_toolkit-0.14.6-py3-none-any.whl", hash = "sha256:764f3a5f9e4b539ce805596863299e8982599514906dc5e3ccc2d390ef74c301", size = 24815, upload-time = "2025-05-12T19:19:13.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trmnl-report"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "cachetools", specifier = ">=5.0.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.12.11" }]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload-time = "2025-04-08T10:36:26.722Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531, upload-time = "2025-04-08T10:35:35.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417, upload-time = "2025-04-08T10:35:37.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423, upload-time = "2025-04-08T10:35:38.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185, upload-time = "2025-04-08T10:35:39.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696, upload-time = "2025-04-08T10:35:41.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327, upload-time = "2025-04-08T10:35:43.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741, upload-time = "2025-04-08T10:35:44.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995, upload-time = "2025-04-08T10:35:46.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693, upload-time = "2025-04-08T10:35:48.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677, upload-time = "2025-04-08T10:35:49.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804, upload-time = "2025-04-08T10:35:51.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload-time = "2025-04-08T10:35:52.458Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue