Compare commits

36 Commits
Author SHA1 Message Date
marc e756f55c19 compose
Docker / build (push) Canceled after 0s
Docker / Deploy (push) Canceled after 0s
2025-05-22 16:13:33 +02:00
marc 64ae3cd521 maj 2025-05-22 15:38:08 +02:00
marc 7976d07b2e dev 2025-03-31 01:29:46 +02:00
marc 9ed7cf5444 priorities in settings 2025-03-19 01:14:39 +01:00
marc 0e90e4c5f9 try implementing apikeyheader for exposing publicly the api 2025-03-19 00:49:06 +01:00
marc 80f82ed12d scheduler import smartevent 2025-03-19 00:48:21 +01:00
marc c2d618b0e5 better settings 2025-03-19 00:47:35 +01:00
marc f5b4f08db5 cleaning smartevent 2025-03-19 00:46:19 +01:00
marc 4ad814edf4 changed events request 2025-03-12 03:33:31 +01:00
marc 63a57675f8 clean 2025-03-12 03:07:17 +01:00
marc d8e50b79a5 created the simplest tests that only require status 200 to pass 2025-03-12 03:04:32 +01:00
marc 8cb02ba558 use of pydantic settings and portion interval more wisely 2025-03-12 02:46:38 +01:00
marc c13dea824c use of pydantic settings and removed commented code 2025-03-11 17:24:56 +01:00
marc a4c0820cd3 top priority done right 2025-03-11 16:57:12 +01:00
marc 05de44a11c top priority 2025-03-11 16:51:43 +01:00
marc 197a530f81 try to archive done cards 2025-03-11 16:44:37 +01:00
marc 827f5f3fa9 2 new priorities 2025-03-04 15:59:29 +01:00
marc 43c947ab9d paths app 2025-03-04 05:37:26 +01:00
marc 4373c31b67 justfile 2025-03-04 05:33:57 +01:00
marc de4825859c dont test python in docker workflow 2025-03-04 05:31:28 +01:00
marc d05e5a30c3 test python 2025-03-04 05:29:29 +01:00
marc 3e9945f024 message ok 2025-03-04 05:26:01 +01:00
marc 77eb3cdac5 workflow 2025-03-04 05:15:52 +01:00
marc bbffe05bdb healthcheck that works 2025-03-04 05:08:40 +01:00
marc d1bbe3bf35 healthcheck that works 2025-03-04 05:07:33 +01:00
marc 32c92a6b7f health 2025-03-04 04:47:11 +01:00
marc 35a15c0040 requirements.txt granian 2025-03-04 04:34:34 +01:00
marc 4f8d84e898 only use granian and remove icalendar 2025-03-04 04:33:43 +01:00
marc abe89fdbba ignore 2025-03-04 04:31:24 +01:00
marc c886fd1e8a remove hypercorn 2025-03-04 04:31:19 +01:00
marc f50aedc5fc dont use granian ci but python module system in dockerfile 2025-03-04 04:30:55 +01:00
marc 4d1b013d38 ignore pytest cache 2025-03-04 04:29:52 +01:00
marc c40b4d68b5 can use python -m app.main 2025-03-04 04:21:39 +01:00
marc b406fd7a06 uv now 2025-03-04 04:21:17 +01:00
marc 74180ae7ea ports 8000 2025-03-04 03:38:02 +01:00
marc 9bf1c8861c use uv instead of hypercorn its faster (#1)
* dockerfile uv

* uv that works
2025-03-04 03:36:37 +01:00
26 changed files with 1454 additions and 2233 deletions
+3
View File
@@ -0,0 +1,3 @@
Makefile
.gitignore
.env
+7
View File
@@ -0,0 +1,7 @@
liberapay: marcpartensky
patreon: marcpartensky
github: marcpartensky
custom:
# - monero:43J2XmRtjDPRLD35UsYGH2AZamFBQz7dt4VdvZXVm3SuadTrvcoXUXBRUNHqukFte23Q5TWXqhbwaLJJgoVQzzbe2AJDANQ
- https://paypal.me/marcpartensky
- https://buymeacoffee.com/marcpartensky
+64
View File
@@ -0,0 +1,64 @@
name: Docker
on:
push:
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
pull_request:
branches:
- master
env:
IMAGE_NAME: time-assistant
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the tag
id: tag
run: echo "::set-output name=tag::$(git rev-parse --short HEAD).$(date +'%Y-%m-%d')"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push with tag
id: tag-build
uses: docker/build-push-action@v2
with:
push: true
tags: marcpartensky/time-assistant:v${{ github.run_number }}.${{ steps.tag.outputs.tag }}
- name: Build and push latest
id: latest-build
uses: docker/build-push-action@v2
with:
push: true
tags: marcpartensky/time-assistant:latest
deploy:
name: Deploy
runs-on: ubuntu-latest
needs:
- build
steps:
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY}}
port: ${{ secrets.PORT }}
script: |
cd ~/git/docker/services
docker compose -f time-assistant/compose.yml pull
docker compose -f time-assistant/compose.yml up -d --force-recreate time-assistant
+38
View File
@@ -0,0 +1,38 @@
name: Test python
on:
push:
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
paths:
- 'app/**' # Trigger only when files in the app folder change
pull_request:
branches:
- master
jobs:
test-python-versions:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.12']
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
cache: pip
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ hashFiles('setup.py') }}-${{ hashFiles('requirements.txt') }}
- name: Install requirements
run: pip install -r requirements.txt
- name: Display Python version
run: python -c "import sys; print(sys.version)"
+1
View File
@@ -1,3 +1,4 @@
.venv
__pycache__/
.pytest_cache
.env
+7 -3
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim as builder
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y curl && \
curl -LsSf https://astral.sh/uv/install.sh | sh && \
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y curl && \
ENV PATH="/root/.cargo/bin:$PATH"
WORKDIR /app
RUn pip install uv granian
RUN pip install uv granian
COPY requirements.txt .
RUN uv pip install -r requirements.txt --target /opt/dependencies
@@ -19,5 +19,9 @@ ENV PYTHONPATH="/opt/dependencies:$PYTHONPATH"
COPY --from=builder /opt/dependencies /opt/dependencies
RUN pip install granian
COPY ./tests/health.py /tests/health.py
HEALTHCHECK CMD python /tests/health.py
COPY ./app /app
ENTRYPOINT ["granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "8000", "--http", "auto", "app.main:app"]
# ENTRYPOINT ["granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "8000", "--http", "auto", "app.main:app"]
ENTRYPOINT ["python", "-m", "app.main"]
+16
View File
@@ -0,0 +1,16 @@
# include .env
# export
# uv run python ./time-assistant/server.py
# uv run uvicorn app.main:app --reload
# hypercorn app.main --bind 0.0.0.0 --reload --log-level debug
# hypercorn -c hypercorn.toml --reload app.main
# granian app.main --interface asgi --host 0.0.0.0 --access-log --reload
run:
python -m app.main
test:
uv run pytest tests/
push:
uv export -o requirements.txt
docker compose build
docker compose push
+13 -6
View File
@@ -1,9 +1,16 @@
# include .env
# export
# uv run python ./time-assistant/server.py
# uv run uvicorn app.main:app --reload
# hypercorn app.main --bind 0.0.0.0 --reload --log-level debug
# hypercorn -c hypercorn.toml --reload app.main
# granian app.main --interface asgi --host 0.0.0.0 --access-log --reload
run:
# uv run python ./time-assistant/server.py
# uv run uvicorn app.main:app --reload
# hypercorn app.main --bind 0.0.0.0 --reload --log-level debug
hypercorn -c hypercorn.toml --reload app.main
python -m app.main
test:
uv run pytest tests/
export:
poetry export -o requirements.txt
push:
uv export -o requirements.txt
docker compose build
docker compose push
-12
View File
@@ -1,12 +0,0 @@
import os
from dotenv import load_dotenv
load_dotenv()
NEXTCLOUD_URL = os.getenv("NEXTCLOUD_URL")
NEXTCLOUD_USER = os.getenv("NEXTCLOUD_USER")
NEXTCLOUD_PASSWORD = os.getenv("NEXTCLOUD_PASSWORD")
NEXTCLOUD_BOARD = os.getenv("NEXTCLOUD_BOARD")
if not all([NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASSWORD, NEXTCLOUD_BOARD]):
raise Exception("Missing required environment variables.")
+15 -2
View File
@@ -16,6 +16,19 @@ app.include_router(websocket_router)
# Point d'entrée principal
if __name__ == "__main__":
import uvicorn
import os
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
from granian import Granian
from granian.constants import Interfaces
from dotenv import load_dotenv
load_dotenv()
Granian(
target="app.main:app",
interface=Interfaces.ASGI,
address=os.environ["GRANIAN_HOST"],
port=int(os.environ["GRANIAN_PORT"]),
log_access=bool(os.environ["GRANIAN_LOG_ACCESS_ENABLED"]),
).serve()
+101
View File
@@ -0,0 +1,101 @@
from datetime import datetime
import portion as P
from zoneinfo import ZoneInfo
from app.settings import settings
# from typing import Iterable
class Interval(P.Interval):
"""Intervalle personnalisé avec un affichage lisible et gestion du timezone."""
def __init__(self, *args, tz: ZoneInfo = settings.zoneinfo, **kwargs):
super().__init__(*args, **kwargs)
self.tz = tz
# %% Méthodes critiques à surcharger %%#
def _convert(self, other):
"""Convertit les autres intervalles en instances de notre classe si nécessaire"""
if isinstance(other, P.Interval) and not isinstance(other, Interval):
return Interval(other, tz=self.tz)
return other
def __or__(self, other):
return Interval(super().__or__(self._convert(other)), tz=self.tz)
def __and__(self, other):
return Interval(super().__and__(self._convert(other)), tz=self.tz)
def __sub__(self, other):
return Interval(super().__sub__(self._convert(other)), tz=self.tz)
def __invert__(self):
return Interval(super().__invert__(), tz=self.tz)
def __str__(self) -> str:
return self.format_intervals()
def format_intervals(self, date_format: str = "%a %d %b %Y") -> str:
if self.empty:
return "∅ (empty interval)"
days = {}
for atomic in self:
start = (
atomic.lower.astimezone(self.tz)
if isinstance(atomic.lower, datetime)
else atomic.lower
)
end = (
atomic.upper.astimezone(self.tz)
if isinstance(atomic.upper, datetime)
else atomic.upper
)
# Groupement par date
if isinstance(start, datetime):
day_key = start.strftime("%Y-%m-%d")
time_slot = (
start.strftime("%H:%M"),
end.strftime("%H:%M") if isinstance(end, datetime) else str(end),
)
if day_key not in days:
days[day_key] = {"date": start, "slots": []}
days[day_key]["slots"].append(time_slot)
# Construction de la sortie
output = []
for day in sorted(days.values(), key=lambda d: d["date"]):
date_str = day["date"].strftime(date_format)
slots = []
for start, end in day["slots"]:
slots.append(f"{start} - {end}")
output.append(f"{date_str}: {', '.join(slots)}")
return "\n".join(output) or "∅ (no datetime intervals)"
@classmethod
def closed(cls, lower, upper):
tz = None
if isinstance(lower, datetime) and lower.tzinfo:
tz = lower.tzinfo
return cls(P.closed(lower, upper), tz)
def to_json(self) -> list[tuple[datetime]]:
"""Sérialise l'intervalle en format JSON exploitable."""
output = []
for atomic in self:
start = atomic.lower
end = atomic.upper
# Conversion des datetime en ISO format
def convert(value):
if isinstance(value, datetime):
return value.astimezone(self.tz).isoformat()
return value
output.append((convert(start), convert(end)))
return output
+203
View File
@@ -0,0 +1,203 @@
from ics import Event
import caldav
from typing import Optional
import vobject
from datetime import datetime
from app.settings import settings
def get_calendar(name: str) -> Optional[caldav.Calendar]:
"""Get calendar by name using list comprehension"""
return next((c for c in settings.calendars if c.name == name), None)
class SmartEvent(Event):
def __init__(
self,
*args,
rrule=None,
exdates=None,
sequence=None,
**kwargs,
):
"""
rrule_obj : un objet dateutil.rrule (optionnel).
"""
super().__init__(*args, **kwargs)
if rrule is not None:
self._rrule = rrule
self.exdates = exdates
self.sequence = sequence
@classmethod
def from_icsevent(cls, event: Event):
return cls(
uid=event.uid,
created=event.created,
last_modified=event.last_modified,
name=event.name,
begin=event.begin,
end=event.end,
# duration=event.duration,
# all_day=event.all_day,
description=event.description,
location=event.location,
url=event.url,
status=event.status,
classification=event.classification,
organizer=event.organizer,
attendees=event.attendees,
alarms=event.alarms,
categories=event.categories,
)
@property
def rrule(self):
# Pour simplifier, on retourne la chaîne RRULE actuelle
return convert_rrule_to_str(self._rrule)
# @rrule.setter
def set_rrule(self, rrule):
self._rrule = rrule
def save_to_calendar(self, calendar_name: str):
"""Sauvegarde dans un calendrier"""
calendar = get_calendar(calendar_name)
if not calendar:
return False
vcal = self.to_vobject()
data = vcal.serialize()
calendar.add_event(data)
return True
def __str__(self):
t = "SmartEvent("
t += self.begin.strftime("%m-%d %H:%M") + ", "
t += self.end.strftime("%m-%d %H:%M") + ", "
t += self.name
t += ")"
return t
def to_vobject(self) -> vobject.base.Component:
"""Convertit un événement ICS.py en VCALENDAR avec gestion des types"""
vcal = vobject.iCalendar()
vevent = vcal.add("vevent")
# Propriétés obligatoires avec conversion explicite
vevent.add("uid").value = str(self.uid)
vevent.add("dtstamp").value = datetime.now(settings.zoneinfo)
vevent.add("summary").value = str(self.name)
vevent.add("dtstart").value = self.begin.datetime
if self.end:
vevent.add("dtend").value = self.end.datetime
if self.sequence:
vevent.add("sequence").value = str(self.sequence)
if self.description:
vevent.add("description").value = str(self.description)
if self.location:
vevent.add("location").value = str(self.location)
if self.categories:
# print("categories:")
# print(self.categories)
# print(type(self.categories))
# vevent.add("categories").value = ",".join(map(str, tuple(self.categories)))
vevent.add("categories").value = self.categories
if hasattr(self, "status"):
if self.status:
vevent.add("status").value = self.status
if hasattr(self, "transparent"):
vevent.add("transp").value = "TRANSPARENT" if self.transparent else "OPAQUE"
return vcal
# # Géolocalisation avec formatage contrôlé
if hasattr(self, "geo"):
if self.geo:
if self.geo.latitude and self.geo.longitude:
geo_value = f"{float(self.geo.latitude):.6f};{float(self.geo.longitude):.6f}"
vevent.add("geo").value = geo_value
if hasattr(self, "rrule"):
rrule_str = str(self.rrule).replace("RRULE:", "")
vevent.add("rrule").value = rrule_str
if hasattr(self, "exdates"):
if self.exdates:
for exdate in self.exdates:
exdate_prop = vevent.add("exdate")
exdate_prop.value = exdate.astimezone(gettz("UTC"))
# # Alarmes avec vérification de type
for alarm in self.alarms:
valarm = vevent.add("valarm")
valarm.add("action").value = "DISPLAY"
valarm.add("trigger").value = alarm.trigger
if hasattr(alarm, "display_text"):
valarm.add("description").value = str(alarm.display_text)
extended_props = {
"url": "url",
"priority": "priority",
"class": "class",
"created": "created",
"last_modified": "last-modified",
}
for attr, prop in extended_props.items():
if hasattr(self, attr):
value = getattr(self, attr)
if isinstance(value, list):
value = ",".join(map(str, value))
vevent.add(prop).value = str(value)
# Validation avant sauvegarde
if len(vevent.contents.get("dtstart", [])) > 1:
raise ValueError("Multiple DTSTART properties detected")
return vcal
def convert_rrule_to_str(rule):
"""
Convertit un objet dateutil.rrule en chaîne conforme à RFC5545.
Attention : ici on utilise des attributs internes de l'objet rrule.
"""
freq_map = {
0: "SECONDLY",
1: "MINUTELY",
2: "HOURLY",
3: "DAILY",
4: "WEEKLY",
5: "MONTHLY",
6: "YEARLY",
}
parts = []
freq = rule._freq
parts.append("FREQ=" + freq_map.get(freq, "DAILY"))
if rule._interval != 1:
parts.append("INTERVAL=" + str(rule._interval))
if rule._until:
parts.append("UNTIL=" + rule._until.strftime("%Y%m%dT%H%M%SZ"))
if rule._count:
parts.append("COUNT=" + str(rule._count))
if rule._byweekday:
day_map = {0: "MO", 1: "TU", 2: "WE", 3: "TH", 4: "FR", 5: "SA", 6: "SU"}
weekdays = []
for wd in rule._byweekday:
weekdays.append(day_map.get(wd.weekday, "MO"))
parts.append("BYDAY=" + ",".join(weekdays))
return ";".join(parts)
+14
View File
@@ -0,0 +1,14 @@
from fastapi import Security, HTTPException, status
from fastapi.security import APIKeyHeader
from db import check_api_key, get_user_from_api_key
api_key_header = APIKeyHeader(name="X-API-Key")
def get_user(api_key_header: str = Security(api_key_header)):
if check_api_key(api_key_header):
user = get_user_from_api_key(api_key_header)
return user
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing or invalid API key"
)
+101 -22
View File
@@ -1,4 +1,6 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from fastapi.security import APIKeyHeader
from app.models.schemas import Message
from pydantic import BaseModel
from collections import OrderedDict
@@ -10,13 +12,31 @@ from app.services.logger import logger
from app.services.deck import nc
import app.services.deck as deck
import app.services.calendar as calendar
from app.services.scheduler import Scheduler
from app.models.interval import Interval
import datetime
import portion as P
from app.settings import settings
router = APIRouter()
ZONEINFO = ZoneInfo(settings.timezone)
# Configuration de la sécurité
api_key_header = APIKeyHeader(name="X-API-Key")
async def get_api_key(api_key: str = Depends(api_key_header)):
if api_key != settings.api_key:
raise HTTPException(status_code=401, detail="Invalid API Key")
return api_key
if settings.debug: # pas de check en debug
router = APIRouter()
else:
router = APIRouter(dependencies=[Depends(get_api_key)])
def serialize_interval(interval: P.Interval) -> Tuple[str]:
@@ -28,18 +48,13 @@ async def root():
return {"message": "Bienvenue dans mon projet FastAPI !"}
@router.get("/live")
@router.get("/live", dependencies=[])
async def health():
return "OK"
@router.post("/echo")
async def echo_message(message: Message):
return {"message": message.text}
return {"message": "OK"}
@router.get("/boards")
async def get_all_boards():
async def get_boards():
return deck.get_boards()
@@ -78,7 +93,7 @@ async def get_cards_by_stack_id(stack_id: str):
@router.get("/board/{board_name}/labels")
async def get_labels(board_name: str):
return deck.get_board_labels(board_name)
return deck.get_labels_by_board_name(board_name)
class Color(BaseModel):
@@ -95,18 +110,30 @@ async def show_color(color: Color):
return color
@router.get("/events")
async def get_events():
@router.get("/all-events")
async def get_all_events():
events = calendar.get_events()
for event in events:
print(event)
return events
@router.get("/events/{days}")
async def get_events(days: int = 1):
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
scheduler = Scheduler(events)
events = scheduler.get_today_events()
print(type(events[0]))
return events
@router.get("/gaps")
@router.get("/gaps/{days}")
async def get_gaps(days: int = 1):
t1 = datetime.datetime.now(ZoneInfo("Europe/Paris"))
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
@@ -118,7 +145,7 @@ async def get_gaps(days: int = 1):
@router.get("/trimed-gaps")
@router.get("/trimed-gaps/{days}")
async def get_trimed_gaps(days: int = 1):
t1 = datetime.datetime.now(ZoneInfo("Europe/Paris"))
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
@@ -134,9 +161,41 @@ async def get_trimed_gaps(days: int = 1):
return [serialize_interval(gap) for gap in gaps]
@router.get("/eat-gaps/{days}")
async def eat_gaps(days: int = 1):
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
scheduler = Scheduler(events)
lunch_interval = scheduler.create_daily_events(
t1, t2, settings.lunch_start, settings.lunch_end
)
dinner_interval = scheduler.create_daily_events(
t1, t2, settings.dinner_start, settings.dinner_end
)
interval = Interval(lunch_interval | dinner_interval)
print(interval)
return interval.to_json()
@router.get("/availability/{days}")
async def availability(days: int = 1):
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
scheduler = Scheduler(events)
interval = scheduler.get_availability(t1, t2)
print(type(interval))
return interval.to_json()
@router.get("/schedule/{board_name}/{days}")
async def schedule(board_name: str, days: int = 1):
t1 = datetime.datetime.now(ZoneInfo("Europe/Paris"))
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
board = deck.get_board_by_name(board_name)
@@ -146,9 +205,6 @@ async def schedule(board_name: str, days: int = 1):
scheduler.hydrate(board)
events = scheduler.schedule(t1, t2)
gaps = scheduler.get_gaps(t1, t2)
gaps = scheduler.trim_timegaps(gaps)
return events
@@ -160,7 +216,7 @@ async def modify():
@router.get("/sleep")
async def sleep():
days = 1
t1 = datetime.datetime.now(ZoneInfo("Europe/Paris"))
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
events = calendar.get_events(t1, t2)
@@ -185,6 +241,29 @@ async def sort_by_priority(board_name: str):
async def clean(days: int = 1):
events = calendar.get_events()
scheduler = Scheduler(events)
t1 = datetime.datetime.now(ZoneInfo("Europe/Paris"))
t1 = datetime.datetime.now(ZONEINFO)
t2 = t1 + datetime.timedelta(days)
return scheduler.clean_deck_events(t1, t2)
@router.get("/top-priority/{board_name}")
async def get_top_priority(board_name: str):
"""Return the top priority card to do."""
return deck.get_top_priority(board_name)
@router.get("/current-events")
async def get_current_event():
t = datetime.datetime.now(ZONEINFO)
return calendar.get_events_at(t)
@router.get("/focus-event")
async def get_focus():
t = datetime.datetime.now(ZONEINFO)
return calendar.get_focus_event_at(t)
# @router.get("/archive-done-tasks/{board_name}")
# async def archive_done_tasks(board_name: str):
# return deck.archive_done_tasks(board_name)
+155 -232
View File
@@ -1,32 +1,26 @@
import caldav
import vobject
# from itertools import filter
# import itertools
from dateutil.rrule import rrule, DAILY, WEEKLY, MO, TU, WE, TH, FR, SA, SU
from dateutil.tz import gettz
from datetime import datetime, timedelta
from ics import Calendar, Event
from ics import Calendar
from ics.alarm import DisplayAlarm
from ics.grammar.parse import ContentLine
from collections import OrderedDict
from typing import List, Optional, Dict
from nextcloud import NextCloud
# import requests
from typing import List, Optional, Dict, Iterator
# from ics.grammar.parse import ContentLine
# from collections import OrderedDict
# from nextcloud import NextCloud
# from app.services.logger import logger
from app.env import *
CALENDAR_NAME = "Personal"
CALENDAR_URL = f"{NEXTCLOUD_URL}/remote.php/dav/calendars/{NEXTCLOUD_USER}"
from app.settings import settings
from app.models.smartevent import SmartEvent
print(CALENDAR_URL)
client = caldav.DAVClient(
CALENDAR_URL, username=NEXTCLOUD_USER, password=NEXTCLOUD_PASSWORD
)
client = settings.caldav_client
principal = client.principal()
calendars = principal.calendars()
@@ -39,235 +33,132 @@ end_of_day = datetime.combine(today, datetime.max.time()) # 23:59:59
# )
class SmartEvent(Event):
def __init__(
self,
*args,
rrule=None,
exdates=None,
sequence=None,
**kwargs,
):
"""
rrule_obj : un objet dateutil.rrule (optionnel).
"""
super().__init__(*args, **kwargs)
if rrule is not None:
self._rrule = rrule
self.exdates = exdates
self.sequence = sequence
@classmethod
def from_icsevent(cls, event: Event):
return cls(
uid=event.uid,
created=event.created,
last_modified=event.last_modified,
name=event.name,
begin=event.begin,
end=event.end,
# duration=event.duration,
# all_day=event.all_day,
description=event.description,
location=event.location,
url=event.url,
status=event.status,
classification=event.classification,
organizer=event.organizer,
attendees=event.attendees,
alarms=event.alarms,
categories=event.categories,
)
@property
def rrule(self):
# Pour simplifier, on retourne la chaîne RRULE actuelle
return convert_rrule_to_str(self._rrule)
def set_rrule(self, rrule):
self._rrule = rrule
def save_to_calendar(self, calendar_name: str):
"""Sauvegarde dans un calendrier"""
calendar = get_calendar(calendar_name)
if not calendar:
return False
vcal = ics_to_vobject(self)
data = vcal.serialize()
calendar.add_event(data)
return True
def __str__(self):
t = "SmartEvent("
t += self.begin.strftime("%Y-%m-%d %H:%M") + ", "
t += self.end.strftime("%Y-%m-%d %H:%M") + ", "
t += self.name
t += ")"
return t
def convert_rrule_to_str(rule):
"""
Convertit un objet dateutil.rrule en chaîne conforme à RFC5545.
Attention : ici on utilise des attributs internes de l'objet rrule.
"""
freq_map = {
0: "SECONDLY",
1: "MINUTELY",
2: "HOURLY",
3: "DAILY",
4: "WEEKLY",
5: "MONTHLY",
6: "YEARLY",
}
parts = []
freq = rule._freq
parts.append("FREQ=" + freq_map.get(freq, "DAILY"))
if rule._interval != 1:
parts.append("INTERVAL=" + str(rule._interval))
if rule._until:
parts.append("UNTIL=" + rule._until.strftime("%Y%m%dT%H%M%SZ"))
if rule._count:
parts.append("COUNT=" + str(rule._count))
if rule._byweekday:
day_map = {0: "MO", 1: "TU", 2: "WE", 3: "TH", 4: "FR", 5: "SA", 6: "SU"}
weekdays = []
for wd in rule._byweekday:
weekdays.append(day_map.get(wd.weekday, "MO"))
parts.append("BYDAY=" + ",".join(weekdays))
return ";".join(parts)
def get_calendar(name: str) -> Optional[caldav.Calendar]:
"""Get calendar by name using list comprehension"""
return next((c for c in client.principal().calendars() if c.name == name), None)
return next((c for c in settings.calendars if c.name == name), None)
def modify_event(
event_uid: str, new_event: SmartEvent, calendar_name: str = CALENDAR_NAME
) -> bool:
"""
Modifie un événement en utilisant un objet ics.Event
"""
calendar = get_calendar(calendar_name)
if not calendar:
return False
# def modify_event(
# event_uid: str,
# new_event: SmartEvent,
# calendar_name: str = settings.personal_calendar,
# ) -> bool:
# """
# Modifie un événement en utilisant un objet ics.Event
# """
# calendar = get_calendar(calendar_name)
# if not calendar:
# return False
# Récupérer l'événement existant
dav_event = calendar.event_by_uid(event_uid)
if not dav_event:
return False
# # Récupérer l'événement existant
# dav_event = calendar.event_by_uid(event_uid)
# if not dav_event:
# return False
# Convertir l'événement ics en vobject
# ics_cal = Calendar(events=[new_event])
vcal = ics_to_vobject(new_event)
vevent = vcal.vevent
# # Convertir l'événement ics en vobject
# # ics_cal = Calendar(events=[new_event])
# vcal = ics_to_vobject(new_event)
# vevent = vcal.vevent
vevent.uid.value = event_uid
# vevent.uid.value = event_uid
data = vcal.serialize()
dav_event.data = data
dav_event.save()
return True
# data = vcal.serialize()
# dav_event.data = data
# dav_event.save()
# return True
def ics_to_vobject(ics_event: SmartEvent) -> vobject.base.Component:
"""Convertit un événement ICS.py en VCALENDAR avec gestion des types"""
vcal = vobject.iCalendar()
vevent = vcal.add("vevent")
# def ics_to_vobject(ics_event: SmartEvent) -> vobject.base.Component:
# """Convertit un événement ICS.py en VCALENDAR avec gestion des types"""
# vcal = vobject.iCalendar()
# vevent = vcal.add("vevent")
# Propriétés obligatoires avec conversion explicite
vevent.add("uid").value = str(ics_event.uid)
vevent.add("dtstamp").value = datetime.now(gettz("UTC"))
vevent.add("summary").value = str(ics_event.name)
# # Propriétés obligatoires avec conversion explicite
# vevent.add("uid").value = str(ics_event.uid)
# vevent.add("dtstamp").value = datetime.now(gettz("UTC"))
# vevent.add("summary").value = str(ics_event.name)
vevent.add("dtstart").value = ics_event.begin.datetime
if ics_event.end:
vevent.add("dtend").value = ics_event.end.datetime
# vevent.add("dtstart").value = ics_event.begin.datetime
# if ics_event.end:
# vevent.add("dtend").value = ics_event.end.datetime
if ics_event.sequence:
vevent.add("sequence").value = str(ics_event.sequence)
# if ics_event.sequence:
# vevent.add("sequence").value = str(ics_event.sequence)
if ics_event.description:
vevent.add("description").value = str(ics_event.description)
# if ics_event.description:
# vevent.add("description").value = str(ics_event.description)
if ics_event.location:
vevent.add("location").value = str(ics_event.location)
# if ics_event.location:
# vevent.add("location").value = str(ics_event.location)
if ics_event.categories:
# print("categories:")
# print(ics_event.categories)
# print(type(ics_event.categories))
# vevent.add("categories").value = ",".join(map(str, tuple(ics_event.categories)))
vevent.add("categories").value = ics_event.categories
# if ics_event.categories:
# # print("categories:")
# # print(ics_event.categories)
# # print(type(ics_event.categories))
# # vevent.add("categories").value = ",".join(map(str, tuple(ics_event.categories)))
# vevent.add("categories").value = ics_event.categories
if hasattr(ics_event, "status"):
if ics_event.status:
vevent.add("status").value = ics_event.status
# if hasattr(ics_event, "status"):
# if ics_event.status:
# vevent.add("status").value = ics_event.status
if hasattr(ics_event, "transparent"):
vevent.add("transp").value = (
"TRANSPARENT" if ics_event.transparent else "OPAQUE"
)
# if hasattr(ics_event, "transparent"):
# vevent.add("transp").value = (
# "TRANSPARENT" if ics_event.transparent else "OPAQUE"
# )
return vcal
# return vcal
# # Géolocalisation avec formatage contrôlé
if hasattr(ics_event, "geo"):
if ics_event.geo:
if ics_event.geo.latitude and ics_event.geo.longitude:
geo_value = f"{float(ics_event.geo.latitude):.6f};{float(ics_event.geo.longitude):.6f}"
vevent.add("geo").value = geo_value
# # # Géolocalisation avec formatage contrôlé
# if hasattr(ics_event, "geo"):
# if ics_event.geo:
# if ics_event.geo.latitude and ics_event.geo.longitude:
# geo_value = f"{float(ics_event.geo.latitude):.6f};{float(ics_event.geo.longitude):.6f}"
# vevent.add("geo").value = geo_value
if hasattr(ics_event, "rrule"):
rrule_str = str(ics_event.rrule).replace("RRULE:", "")
vevent.add("rrule").value = rrule_str
# if hasattr(ics_event, "rrule"):
# rrule_str = str(ics_event.rrule).replace("RRULE:", "")
# vevent.add("rrule").value = rrule_str
if hasattr(ics_event, "exdates"):
if ics_event.exdates:
for exdate in ics_event.exdates:
exdate_prop = vevent.add("exdate")
exdate_prop.value = exdate.astimezone(gettz("UTC"))
# if hasattr(ics_event, "exdates"):
# if ics_event.exdates:
# for exdate in ics_event.exdates:
# exdate_prop = vevent.add("exdate")
# exdate_prop.value = exdate.astimezone(gettz("UTC"))
# # Alarmes avec vérification de type
for alarm in ics_event.alarms:
valarm = vevent.add("valarm")
valarm.add("action").value = "DISPLAY"
valarm.add("trigger").value = alarm.trigger
# # # Alarmes avec vérification de type
# for alarm in ics_event.alarms:
# valarm = vevent.add("valarm")
# valarm.add("action").value = "DISPLAY"
# valarm.add("trigger").value = alarm.trigger
if hasattr(alarm, "display_text"):
valarm.add("description").value = str(alarm.display_text)
# if hasattr(alarm, "display_text"):
# valarm.add("description").value = str(alarm.display_text)
extended_props = {
"url": "url",
"priority": "priority",
"class": "class",
"created": "created",
"last_modified": "last-modified",
}
# extended_props = {
# "url": "url",
# "priority": "priority",
# "class": "class",
# "created": "created",
# "last_modified": "last-modified",
# }
for attr, prop in extended_props.items():
if hasattr(ics_event, attr):
value = getattr(ics_event, attr)
if isinstance(value, list):
value = ",".join(map(str, value))
vevent.add(prop).value = str(value)
# for attr, prop in extended_props.items():
# if hasattr(ics_event, attr):
# value = getattr(ics_event, attr)
# if isinstance(value, list):
# value = ",".join(map(str, value))
# vevent.add(prop).value = str(value)
# Validation avant sauvegarde
if len(vevent.contents.get("dtstart", [])) > 1:
raise ValueError("Multiple DTSTART properties detected")
# # Validation avant sauvegarde
# if len(vevent.contents.get("dtstart", [])) > 1:
# raise ValueError("Multiple DTSTART properties detected")
return vcal
# return vcal
def get_events(
# calendar_name: str = CALENDAR_NAME,
# calendar_name: str = PERSONAL_CALENDAR,
start: datetime = start_of_day,
end: datetime = end_of_day,
) -> list[SmartEvent]:
@@ -283,25 +174,47 @@ def get_events(
return events
def get_events_at(t: datetime) -> Iterator[SmartEvent]:
for event in get_events():
if event.begin < t < event.end:
yield event
def get_focus_event_at(t: datetime) -> Optional[SmartEvent]:
day = timedelta(days=1)
coming_events = (
e
for e in get_events()
if e.end >= t
and ( # not a full day
((e.end - e.begin) % day).seconds != 0
and e.begin == e.begin.replace(minute=0, second=0, microsecond=0)
)
)
for e in sorted(coming_events, key=lambda e: (e.begin, e.end)):
return e
def modify_description():
events = get_events()
if not events:
return []
# Convertir l'événement caldav en ics.Event
original_event = events[0]
event = SmartEvent(
name="Nouveau nom d'événement",
begin=original_event.begin.datetime,
end=original_event.end.datetime,
description="Nouvelle description",
location="Nouveau lieu",
# status=None,
# status="CONFIRMED",
alarms=[
DisplayAlarm(trigger=timedelta(minutes=-15), display_text="allumer le feu"),
],
)
# original_event = events[0]
# event = SmartEvent(
# name="Nouveau nom d'événement",
# begin=original_event.begin.datetime,
# end=original_event.end.datetime,
# description="Nouvelle description",
# location="Nouveau lieu",
# # status=None,
# # status="CONFIRMED",
# alarms=[
# DisplayAlarm(trigger=timedelta(minutes=-15), display_text="allumer le feu"),
# ],
# )
# event.rrule = convert_rrule_to_str(
# rrule(
# freq=DAILY,
@@ -321,10 +234,20 @@ def modify_description():
# "until": datetime(2025, 3, 31, 21, 0, tzinfo=gettz("UTC")),
# }
event = events[0]
# event.name = "Nouveau nom d'événement"
event.description = "Nouvelle description"
# event.location = "Nouveau lieu"
# event.alarms=[
# DisplayAlarm(trigger=timedelta(minutes=-15), display_text="allumer le feu"),
# ],
# )
# Appliquer les modifications
success = modify_event(original_event.uid, event)
return {"success": success, "events": get_events()}
# success = modify_event(original_event.uid, event)
# return {"success": success, "events": get_events()}
return event.save_to_calendar("Personal")
if __name__ == "__main__":
print(principal)
def get_current_event_and_time_left():
"""Get the current and event and how much time is left to do the task."""
+25 -11
View File
@@ -5,12 +5,13 @@ from deck.models import Card, Board, Stack, Label
from requests.auth import HTTPBasicAuth
from app.services.logger import logger
from typing import List
from app.env import *
auth = HTTPBasicAuth(os.environ["NEXTCLOUD_USER"], os.environ["NEXTCLOUD_PASSWORD"])
from app.settings import settings
auth = HTTPBasicAuth(settings.nextcloud_user, settings.nextcloud_password)
nc = NextCloudDeckAPI(
NEXTCLOUD_URL,
settings.nextcloud_url,
auth,
ssl_verify=True,
)
@@ -55,9 +56,6 @@ def get_label_id_by_name(board_id: str, label_name: str):
raise LogException(status_code=500, detail=str(e))
NEXTCLOUD_BOARD_ID = get_board_id_by_name(str(NEXTCLOUD_BOARD))
def get_boards():
return nc.get_boards()
@@ -111,11 +109,27 @@ def reorder_card(*args, **kwargs):
return nc.reorder_card(*args, **kwargs)
# def update_card(card_id: int, order: int, title: str, owner: str):
# card_id=card.id,
# order=50,
# title=card.title,
# owner=card.owner,
def get_top_priority(board_name: str) -> Card:
"""Return the top priority card."""
board = get_board_by_name(board_name)
stacks = get_stacks_by_board(board)
stacks.sort(key=lambda stack: stack.order)
cards = stacks[0].cards
cards.sort(key=lambda card: card.order)
return cards[0]
# def archive_done_tasks(board_name: str) -> List[Card]:
# """Read all the cards from the board and find the tasks that are done.
# Then archive them and return them."""
# # L'API nextcloud deck ne permet pas de manipuler le champs done des cartes.
# done_cards = []
# board = get_board_by_name(board_name)
# stacks = get_stacks_by_board(board)
# for stack in stacks:
# card = stack.cards[0]
# return done_cards
# def archive_completed(board_name, str, stack_name: str):
+134 -204
View File
@@ -1,10 +1,7 @@
from .calendar import SmartEvent, Event
from deck.models import Card, Board, Stack, Label
from typing import List, Tuple, NamedTuple
import portion as P
import vobject
import app.services.deck as deck
import app.services.calendar as calendar
@@ -15,26 +12,20 @@ from datetime import datetime, timedelta, time
from zoneinfo import ZoneInfo
import ast
from app.models.interval import Interval
from app.models.smartevent import SmartEvent
from app.settings import settings
# planification uniquement dans ces zones horaires
DEFAULT_WORK_START = 9
DEFAULT_WORK_END = 20
DEFAULT_WORK_START = settings.work_start
DEFAULT_WORK_END = settings.work_end
PLAN_DAYS_LIMIT = 1 # Nombre de jours à planifier
PLAN_DAYS_LIMIT = settings.plan_day_limit # Nombre de jours à planifier
DEFAULT_TASK_DURATION = timedelta(hours=2, minutes=45) # en heure
class TimeGap(NamedTuple):
t1: datetime
t2: datetime
def __str__(self):
return "TG" + str((self.t1, self.t2))
# Class TimeGaps(List(TimeGap)):
# def __
DEFAULT_TASK_DURATION = timedelta(
hours=settings.task_duration_hours, minutes=settings.task_duration_minutes
) # en heure
class Scheduler:
@@ -48,14 +39,21 @@ class Scheduler:
calendars: List[str] = [],
):
self.events = events
self.calendars = calendars
self.work_start = work_start
self.work_stop = work_stop
self.calendars = calendars
self.work_calendar = "Taff"
self.personal_calendar = "Personal"
self.zoneinfo = ZoneInfo("Europe/Paris")
self.priorities = ["Top priority", "High priority"]
self.category = "Deck" # Tag given on all created events
self.lunch_start = settings.lunch_start
self.lunch_end = settings.lunch_end
self.dinner_start = settings.dinner_start
self.dinner_end = settings.dinner_end
self.work_calendar = settings.work_calendar
self.personal_calendar = settings.personal_calendar
self.zoneinfo = settings.zoneinfo
self.priorities = settings.priorities
self.category = settings.deck_category
def hydrate(self, board: Board):
self.board = board
@@ -73,59 +71,16 @@ class Scheduler:
sorted_cards.extend(cards)
self.cards = sorted_cards
# def get_gaps(self, t1: datetime, t2: datetime) -> List[TimeGap]:
# if t1 >= t2:
# return []
# events = [
# (event.begin.astimezone(self.zoneinfo), event.end.astimezone(self.zoneinfo))
# for event in self.events
# ]
# valid_events = [(s, e) for (s, e) in events if s < e]
# sorted_events = sorted(valid_events, key=lambda x: x[0])
# # Merging overlapping events
# merged = []
# for event in sorted_events:
# if not merged:
# merged.append(event)
# else:
# last_start, last_end = merged[-1]
# current_start, current_end = event
# if current_start <= last_end:
# merged[-1] = (last_start, max(last_end, current_end))
# else:
# merged.append(event)
# # Clip the events starting before t1 and after t2
# clipped = []
# for start, end in merged:
# clipped_start = max(start, t1)
# clipped_end = min(end, t2)
# if clipped_start < clipped_end:
# clipped.append((clipped_start, clipped_end))
# gaps = []
# prev_end = t1
# for start, end in clipped:
# if start > prev_end:
# gaps.append(TimeGap(prev_end, start))
# prev_end = max(prev_end, end)
# if prev_end < t2:
# gaps.append(TimeGap(prev_end, t2))
# return gaps
def get_gaps(self, t1: datetime, t2: datetime) -> List[P.Interval]:
def get_gaps(self, t1: datetime, t2: datetime) -> Interval:
"""Trouve les disponibilités entre les événements calendrier."""
if t1 >= t2:
return []
return Interval(P.empty())
# Créer un intervalle global [t1, t2]
total_interval = P.closed(t1, t2)
# Créer l'union de tous les événements
events_union = P.Interval(
events_union = Interval(
*[
P.closed(
event.begin.astimezone(self.zoneinfo),
@@ -135,49 +90,7 @@ class Scheduler:
if event.begin < event.end
]
)
# Calculer les gaps = total_interval - events_union
gaps = total_interval - events_union
return list(gaps) if not gaps.empty else []
def create_sleep_events(self):
"""Create sleep events"""
events = []
today = datetime.today().date()
begin_of_sleep = datetime.combine(today, datetime.min.time()).astimezone(
self.zoneinfo
) # 00:00:00
end_of_sleep = begin_of_sleep + timedelta(hours=9)
gaps = self.get_gaps(begin_of_sleep, end_of_sleep)
print(end_of_sleep)
# print(gaps[0][1] - timed)
first_gap = gaps[0]
if first_gap[1] - timedelta(hours=1) < end_of_sleep:
end_of_sleep = first_gap[1] - timedelta(hours=1)
print(end_of_sleep)
event = Event(name="Sleep", begin=begin_of_sleep, end=end_of_sleep)
target_calendar = calendar.get_calendar(self.personal_calendar)
if not target_calendar:
raise ValueError(f"Calendar '{self.work_calendar}' not found")
events.append(event)
print(event.__dict__)
vcal = calendar.ics_to_vobject(event)
ical_data = vcal.serialize()
target_calendar.add_event(ical_data)
return events
def create_deck_events(self):
events = []
return events
return total_interval - events_union
def sort_card_by_priority(self) -> List[Card]:
"""Tri par ordre de priorité"""
@@ -223,7 +136,7 @@ class Scheduler:
"""Return time estimation of card in seconds."""
return DEFAULT_TASK_DURATION
def create_event(self, card: Card, interval: P.Interval) -> SmartEvent:
def create_event(self, card: Card, interval: Interval) -> SmartEvent:
"""Create new event using a card, t1 and t2."""
categories = [self.category + str(card.id)]
if card.labels:
@@ -267,95 +180,31 @@ class Scheduler:
dav_event.delete()
return removed_events
def trim_timegaps(self, gaps: List[P.Interval]) -> List[P.Interval]:
"""Filtre les gaps pour ne garder que les heures de travail."""
if not gaps:
return []
# def clean_all_deck_events(self) -> int:
# """Remove all vcal events that have a Category Deck* (for instance Deck1234)"""
# calendar_obj = calendar.get_calendar(self.work_calendar)
# if not calendar_obj:
# return 0
# Créer l'union de tous les gaps
gaps_union = P.Interval(*gaps)
# # Récupérer les événements existants
# removed_events = 0
# Générer les plages horaires de travail
work_hours = P.empty()
start_date = min(gap.lower.date() for gap in gaps)
end_date = max(gap.upper.date() for gap in gaps)
current_date = start_date
while current_date <= end_date:
work_start = datetime.combine(
current_date, time(hour=self.work_start)
).replace(tzinfo=self.zoneinfo)
work_end = datetime.combine(
current_date, time(hour=self.work_stop)
).replace(tzinfo=self.zoneinfo)
work_hours |= P.closed(work_start, work_end)
current_date += timedelta(days=1)
# Intersection entre les gaps et les heures de travail
trimmed = gaps_union & work_hours
return list(trimmed) if not trimmed.empty else []
# def schedule(self, t1: datetime, t2: datetime) -> List[SmartEvent]:
# self.clean_deck_events(t1, t2)
# gaps = self.get_gaps(t1, t2)
# gaps = self.trim_timegaps(gaps)
# if not gaps or not self.cards:
# return []
# # Aplatir les intervalles composés
# atomic_gaps = []
# for gap in gaps:
# atomic_gaps.extend(gap)
# events = []
# card_iter = iter(self.cards)
# try:
# current_card = next(card_iter)
# current_duration = self.estimate_duration(current_card)
# for gap in atomic_gaps:
# remaining_gap = gap.upper - gap.lower
# while remaining_gap > timedelta(0):
# if current_duration <= remaining_gap:
# # Créer l'événement complet
# events.append(
# self.create_event(
# current_card, gap.lower, gap.lower + current_duration
# )
# )
# gap = P.closed(gap.lower + current_duration, gap.upper)
# current_card = next(card_iter)
# current_duration = self.estimate_duration(current_card)
# remaining_gap = gap.upper - gap.lower
# else:
# # Utiliser une partie du gap
# events.append(
# self.create_event(
# current_card, gap.lower, gap.lower + remaining_gap
# )
# )
# current_duration -= remaining_gap
# break
# except StopIteration:
# pass
# for event in events:
# event.save_to_calendar(self.work_calendar)
# return events
# for dav_event in self.events:
# vevent = dav_event.vobject_instance.vevent
# if hasattr(vevent, "categories"):
# categories = vevent.categories.value
# # Vérifier les catégories commençant par 'Deck'
# if any(cat.strip().startswith(self.category) for cat in categories):
# removed_events += 1
# dav_event.delete()
# return removed_events
def add_work_breaks(
self,
gaps: List[P.Interval],
gaps: List[Interval],
work_duration: timedelta = DEFAULT_TASK_DURATION,
break_duration: timedelta = timedelta(minutes=15),
) -> List[P.Interval]:
) -> List[Interval]:
"""Découpe les plages horaires en ajoutant des pauses régulières."""
new_gaps = []
@@ -379,12 +228,35 @@ class Scheduler:
return new_gaps
def schedule(self, t1: datetime, t2: datetime) -> List[SmartEvent]:
self.clean_deck_events(t1, t2)
gaps = self.get_gaps(t1, t2)
gaps = self.trim_timegaps(gaps)
gaps = self.add_work_breaks(gaps)
def get_availability(self, t1: datetime, t2: datetime) -> Interval:
"""Consider all parameters to get my availibility interval."""
availability = self.get_gaps(t1, t2)
# eat intervals
lunch_interval = self.create_daily_events(
t1, t2, settings.lunch_start, settings.lunch_end
)
dinner_interval = self.create_daily_events(
t1, t2, settings.dinner_start, settings.dinner_end
)
eat_interval = lunch_interval | dinner_interval
availability -= eat_interval
# sleep
sleep_interval = self.create_sleep_events(t1, t2)
availability -= sleep_interval
# gaps |= self.add_work_breaks(gaps)
return Interval(availability)
def schedule(
self,
t1: datetime,
t2: datetime,
) -> List[SmartEvent]:
"""Schedule my calendar between t1 and t2."""
self.clean_deck_events(t1, t2)
gaps = self.get_availability(t1, t2)
if not gaps or not self.cards:
return []
@@ -431,5 +303,63 @@ class Scheduler:
for event in events:
event.save_to_calendar(self.work_calendar)
return events
def create_daily_events(
self,
t1: datetime,
t2: datetime,
g1: int,
g2: int,
) -> Interval:
"""Create daily events in the schedule."""
daily_events = P.empty()
current_date = t1.date()
end_date = t2.date()
while current_date <= end_date:
# Create daily start and end times for the current date
daily_start_time = time(hour=g1)
daily_end_time = time(hour=g2)
daily_start_dt = datetime.combine(current_date, daily_start_time).replace(
tzinfo=self.zoneinfo
)
daily_end_dt = datetime.combine(current_date, daily_end_time).replace(
tzinfo=self.zoneinfo
)
# Adjust daily_end_dt if it's on the next day
if daily_end_dt <= daily_start_dt:
daily_end_dt += timedelta(days=1)
# Create the interval for the current day's daily
day_daily_interval = P.closed(daily_start_dt, daily_end_dt)
# Intersect with the overall time range [t1, t2]
valid_interval = day_daily_interval & P.closed(t1, t2)
if not valid_interval.empty:
daily_events |= valid_interval
current_date += timedelta(days=1)
return daily_events
def create_sleep_events(self, t1: datetime, t2: datetime) -> Interval:
"""Create sleep events"""
sleep_events = self.create_daily_events(t1, t2, 0, settings.work_start)
# events = self.create_daily_events(t1, t2, settings.work_end, 24)
return sleep_events
def get_today_events(self) -> List[SmartEvent]:
"""Renvoie tous les événements prévus aujourd'hui dans le calendrier."""
now = datetime.now(self.zoneinfo)
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + timedelta(days=1)
return [
event
for event in self.events
if event.begin.astimezone(self.zoneinfo) < end_of_day
and event.end.astimezone(self.zoneinfo) > start_of_day
]
+82
View File
@@ -0,0 +1,82 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List
from dotenv import load_dotenv
from zoneinfo import ZoneInfo
import caldav
load_dotenv()
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="TIMEASSISTANT_")
# Nextcloud parameters are mandatory
nextcloud_user: str
nextcloud_password: str
nextcloud_board: str
nextcloud_url: str
app_name: str = "Time Assistant"
debug: bool = False
log_level: str = "Info"
timezone: str = "Europe/Paris"
board_name: str = "-Todolist"
deck_category: str = "Deck"
personal_calendar: str = "Personal"
work_calendar: str = "Taff"
fitness_calendar: str = "Fitness"
plan_day_limit: int = 1
task_duration_hours: int = 3
task_duration_minutes: int = 0
work_start: int = 9
work_end: int = 20
lunch_start: int = 12
lunch_end: int = 13
dinner_start: int = 20
dinner_end: int = 21
api_key: str = "osef"
priorities: List[str] = [
"Top priority",
"High priority",
"Action needed",
"Low priority",
]
@property
def zoneinfo(self) -> ZoneInfo:
return ZoneInfo(self.timezone)
@property
def calendar_url(self):
return f"{self.nextcloud_url}/remote.php/dav/calendars/{self.nextcloud_user}"
@property
def caldav_client(self) -> caldav.DAVClient:
"""Return caldav client"""
return caldav.DAVClient(
self.calendar_url,
username=self.nextcloud_user,
password=self.nextcloud_password,
)
@property
def principal(self) -> caldav.Principal:
"""Return principal"""
return self.caldav_client.principal()
@property
def calendars(self) -> List[caldav.Calendar]:
"""Return calendars"""
return self.principal.calendars()
settings = Settings()
from pprint import pp
pp(settings)
+2
View File
@@ -3,3 +3,5 @@ services:
image: marcpartensky/time-assistant
build: .
env_file: .env
ports:
- "8000:8000"
-10
View File
@@ -1,10 +0,0 @@
# hypercorn.toml
bind = "0.0.0.0:8000"
workers = 1
http2 = true
accesslog = "-"
# reload = true
[logging]
log_level = "debug"
access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s"
Generated
-1213
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -7,7 +7,6 @@ requires-python = ">=3.12,<4"
dependencies = [
"caldav>=1.4.0",
"fastapi>=0.115.6",
"icalendar>=6.1.0",
"ics>=0.7.2",
"nextcloud-api-wrapper>=0.2.3",
"nextcloud-deck>=0.2.1",
@@ -15,17 +14,19 @@ dependencies = [
"termcolor>=2.5.0",
"websockets>=14.1",
"portion (>=2.6.0,<3.0.0)",
"hypercorn (>=0.17.3,<0.18.0)",
"granian>=2.0.0",
"pydantic-settings>=2.8.1",
]
[tool.uv]
dev-dependencies = [
"black>=24.10.0",
"granian[reload]>=2.0.0",
"httpx>=0.28.1",
"pyright>=1.1.390",
"pytest>=8.3.4",
]
[tool.poetry.group.dev.dependencies]
granian = "^2.0.0"
granian = {extras = ["reload"], version = "^2.0.0"}
+306 -490
View File
@@ -1,515 +1,331 @@
annotated-types==0.7.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
anyio==4.8.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a \
--hash=sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a
arrow==1.3.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 \
--hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85
attrs==21.4.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \
--hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd
caldav==1.4.0 ; python_version >= "3.12" and python_version < "4" \
# This file was autogenerated by uv via the following command:
# uv export -o requirements.txt
annotated-types==0.7.0 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53
anyio==4.7.0 \
--hash=sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48 \
--hash=sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352
arrow==1.3.0 \
--hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 \
--hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80
attrs==21.4.0 \
--hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd \
--hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4
black==24.10.0 \
--hash=sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875 \
--hash=sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3 \
--hash=sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65 \
--hash=sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f \
--hash=sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8 \
--hash=sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981 \
--hash=sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b \
--hash=sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2 \
--hash=sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b \
--hash=sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d
caldav==1.4.0 \
--hash=sha256:4317131127d8793f740cff2fd256f369321fa49ad750f83d6f31780f7c16c67b \
--hash=sha256:e75e84824092e33a9e03ac693de3d01133a3e044fd50a1c542c7f78d1aff0cb2
cattrs==1.10.0 ; python_version >= "3.12" and python_version < "4" \
cattrs==1.10.0 \
--hash=sha256:211800f725cdecedcbcf4c753bbd22d248312b37d130f06045434acb7d9b34e1 \
--hash=sha256:35dd9063244263e63bd0bd24ea61e3015b00272cead084b2c40d788b0f857c46
certifi==2025.1.31 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \
--hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
charset-normalizer==3.4.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \
--hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \
--hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \
--hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \
--hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \
--hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \
--hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \
--hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \
--hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \
--hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \
--hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \
--hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \
--hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \
--hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \
--hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \
--hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \
--hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \
--hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \
--hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \
--hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \
--hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \
--hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \
--hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \
--hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \
--hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \
--hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \
--hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \
--hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
--hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \
--hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \
--hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \
--hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \
--hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \
--hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \
--hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \
--hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \
--hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \
--hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \
--hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \
--hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \
--hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \
--hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \
--hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \
--hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \
--hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \
--hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \
--hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \
--hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \
--hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \
--hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \
--hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \
--hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \
--hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \
--hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \
--hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \
--hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \
--hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \
--hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \
--hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \
--hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \
--hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \
--hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \
--hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \
--hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \
--hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \
--hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \
--hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \
--hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \
--hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \
--hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \
--hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \
--hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \
--hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \
--hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \
--hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
--hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \
--hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \
--hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \
--hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \
--hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \
--hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \
--hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \
--hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \
--hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \
--hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \
--hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \
--hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \
--hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \
--hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \
--hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \
--hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
click==8.1.8 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
colorama==0.4.6 ; python_version >= "3.12" and python_version < "4" and platform_system == "Windows" \
certifi==2024.12.14 \
--hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db \
--hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56
charset-normalizer==3.4.0 \
--hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \
--hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \
--hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \
--hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \
--hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \
--hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \
--hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \
--hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \
--hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \
--hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \
--hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \
--hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \
--hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \
--hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \
--hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \
--hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \
--hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \
--hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \
--hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \
--hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \
--hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \
--hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \
--hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \
--hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \
--hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \
--hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \
--hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \
--hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \
--hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 \
--hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \
--hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \
--hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079
click==8.1.7 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28
colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
dateutils==0.6.12 ; python_version >= "3.12" and python_version < "4" \
dateutils==0.6.12 \
--hash=sha256:03dd90bcb21541bd4eb4b013637e4f1b5f944881c46cc6e4b67a6059e370e3f1 \
--hash=sha256:f33b6ab430fa4166e7e9cb8b21ee9f6c9843c48df1a964466f52c79b2a8d53b3
fastapi==0.115.11 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64 \
--hash=sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f
h11==0.14.0 ; python_version >= "3.12" and python_version < "4" \
fastapi==0.115.6 \
--hash=sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654 \
--hash=sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305
granian==2.0.0 \
--hash=sha256:d040c63e069ce195607530afa53fc1fd50b8e99284184f051e5bd6b4c3742081 \
--hash=sha256:16b71f2e5d0c7e2059cc5902dc61908c4dbb54088c09acc14457c89f1778936d \
--hash=sha256:4c58be99fb385ee219111cdaf6f5b069c668a3345551b4209073a1db83125b94 \
--hash=sha256:6f4695bcf6e2c3842e212dc42253be0400ee68e7b2245555d9d04ffb44e85919 \
--hash=sha256:a7122062532355af2c87048f859f6006a452073af5e88dd4b0b8f8eafb4107b5 \
--hash=sha256:0618855fcf40a0c768839031f5c55cf0debe0b4fc123c129e5a6f0166e341fc5 \
--hash=sha256:6a2d7c51d20cb0fe831d481c980f4ff2c400a4c7f84abad66d8c1ac94f468921 \
--hash=sha256:54bf94c4bbfb2630b33b1a075b4571fa2b1e870cbf06f4145c29e13122aec8ff \
--hash=sha256:f98a6d604b0dbdd226b0a0c823242706bd99070abd6977d4903c4249d231aab4 \
--hash=sha256:3f83644dace8acb07a4bd36c5fdf6731eea785ef243a8c5ee77361eec09c2c35 \
--hash=sha256:f65c7d4b6d5280a622a3e0ed899654221d748cf7bde801d701f2d7266ec0cb8e \
--hash=sha256:b87b851f38e1cf3d48bb49069efb7e4b18db46c664512e3dd6d0fea5ce6806e1 \
--hash=sha256:a71872e7f619f7b8e543fdfcdb0a5025395cd36326f53558474f4c9c243219de \
--hash=sha256:70cdb93c63711f3da3e4b3a465affb514cb933635fdbdbfd0349c4f2920069ab \
--hash=sha256:4d3fc2895de84a15ded59ecc69a100ff5e53cfb6ac9eb96f6db00d571b604b1c \
--hash=sha256:565c90932189b759534096d260837a6af3dac492490fa4858339f5d251181e94 \
--hash=sha256:5769cf26bd27bdfce2932570ec3cf1e6703b493ef9d64acbba7740800299982a \
--hash=sha256:88be218f5daad58b2984aeec27720b35082d47bbb1cd1b6f910168431a94c181 \
--hash=sha256:de8ce3356f8016aababf946caec88d3423102d428e6c988538de83e1345d419a \
--hash=sha256:1598cc112b15b78915a3231d916c1db14c6d00e1b3802118a9610ac4e171dea1 \
--hash=sha256:035f765b7a918c8b197917b3451dd4653db3478b9c8221c2a7f0532472a203a9 \
--hash=sha256:b80e1acdd3ec632abec6ddce9c549351ecf0bd7a24914f338920375a28e35f8b
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
icalendar==6.1.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:2c44355a8f006de5ae73fa3f022a1cbe2a0de6b1607ce8879739eb887c4f3471 \
--hash=sha256:accf3a4be9a1f89bad00e0bf14103b02cd9b02dcb9b4258eb717f39d24cf58e9
ics==0.7.2 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103 \
--hash=sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05
idna==3.10 ; python_version >= "3.12" and python_version < "4" \
httpcore==1.0.7 \
--hash=sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c \
--hash=sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
icalendar==6.1.0 \
--hash=sha256:43c2db8632959d634f4e48f6e6131e706bf2cdddad488cf0b72fda079b796bad \
--hash=sha256:46c09b774a6e6948495dafcb166dc15135c8259d0ae25491f154cbc822714b69
ics==0.7.2 \
--hash=sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05 \
--hash=sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
lxml==5.3.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81 \
--hash=sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131 \
--hash=sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9 \
--hash=sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576 \
--hash=sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1 \
--hash=sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c \
--hash=sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914 \
--hash=sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607 \
--hash=sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8 \
--hash=sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709 \
--hash=sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b \
--hash=sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c \
--hash=sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7 \
--hash=sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2 \
--hash=sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c \
--hash=sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27 \
--hash=sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33 \
--hash=sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8 \
--hash=sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19 \
--hash=sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae \
--hash=sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295 \
--hash=sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c \
--hash=sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b \
--hash=sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70 \
--hash=sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf \
--hash=sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79 \
--hash=sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84 \
--hash=sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d \
--hash=sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423 \
--hash=sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394 \
--hash=sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1 \
--hash=sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270 \
--hash=sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2 \
--hash=sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b \
--hash=sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd \
--hash=sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e \
--hash=sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f \
--hash=sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439 \
--hash=sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61 \
--hash=sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314 \
--hash=sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0 \
--hash=sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2 \
--hash=sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8 \
--hash=sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0 \
--hash=sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc \
--hash=sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32 \
--hash=sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499 \
--hash=sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde \
--hash=sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468 \
--hash=sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe \
--hash=sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de \
--hash=sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877 \
--hash=sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5 \
--hash=sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629 \
--hash=sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7 \
--hash=sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a \
--hash=sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5 \
--hash=sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73 \
--hash=sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f \
--hash=sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6 \
--hash=sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac \
--hash=sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65 \
--hash=sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519 \
--hash=sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5 \
--hash=sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8 \
--hash=sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d \
--hash=sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51 \
--hash=sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf \
--hash=sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20 \
--hash=sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7 \
--hash=sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53 \
--hash=sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e \
--hash=sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5 \
--hash=sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5 \
--hash=sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9 \
--hash=sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2 \
--hash=sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff \
--hash=sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a \
--hash=sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c \
--hash=sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549 \
--hash=sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be \
--hash=sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9 \
--hash=sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125 \
--hash=sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac \
--hash=sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332 \
--hash=sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2 \
--hash=sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce \
--hash=sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5 \
--hash=sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a \
--hash=sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23 \
--hash=sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3 \
--hash=sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c \
--hash=sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414 \
--hash=sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b \
--hash=sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9 \
--hash=sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98 \
--hash=sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e \
--hash=sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3 \
--hash=sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406 \
--hash=sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307 \
--hash=sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098 \
--hash=sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9 \
--hash=sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725 \
--hash=sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd \
--hash=sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e \
--hash=sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322 \
--hash=sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd \
--hash=sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675 \
--hash=sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e \
--hash=sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a \
--hash=sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1 \
--hash=sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a \
--hash=sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb \
--hash=sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe \
--hash=sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6 \
--hash=sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50 \
--hash=sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca \
--hash=sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0 \
--hash=sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78 \
--hash=sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367 \
--hash=sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666 \
--hash=sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645 \
--hash=sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a \
--hash=sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3 \
--hash=sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252 \
--hash=sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79 \
--hash=sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615 \
--hash=sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f \
--hash=sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af \
--hash=sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c \
--hash=sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7 \
--hash=sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407 \
--hash=sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2 \
--hash=sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac \
--hash=sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9 \
--hash=sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5 \
--hash=sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2 \
--hash=sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2
nextcloud-api-wrapper==0.2.3 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:1899650073f1428cb12837bd61b0accd95ca54612752c42f901d7bd49deeea75 \
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
lxml==5.3.0 \
--hash=sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f \
--hash=sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859 \
--hash=sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e \
--hash=sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f \
--hash=sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e \
--hash=sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179 \
--hash=sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a \
--hash=sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3 \
--hash=sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1 \
--hash=sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d \
--hash=sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c \
--hash=sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99 \
--hash=sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff \
--hash=sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a \
--hash=sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8 \
--hash=sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d \
--hash=sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30 \
--hash=sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f \
--hash=sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a \
--hash=sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd \
--hash=sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51 \
--hash=sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b \
--hash=sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002 \
--hash=sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4 \
--hash=sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492 \
--hash=sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3 \
--hash=sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4 \
--hash=sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367 \
--hash=sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832 \
--hash=sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff \
--hash=sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd \
--hash=sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb \
--hash=sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b \
--hash=sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957 \
--hash=sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d
mypy-extensions==1.0.0 \
--hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 \
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d
nextcloud-api-wrapper==0.2.3 \
--hash=sha256:30ac51f86a7a00506a376034f804cc1fe70385496443fb75de6064d7c1f97a6e \
--hash=sha256:a30e66e4fbe451692c050c15b2dc6130df5547bd53e29db548d8141303c03e48
nextcloud-deck==0.2.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:39ec92c51e7b85081a88697f4e7d8167213b1c7782e1a60ca89169caca4c61ba \
--hash=sha256:b1f49c8cb1833a612c2db176d1ed77ef27dde4894f61c0999cf05c9f513f8f46
portion==2.6.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:1899650073f1428cb12837bd61b0accd95ca54612752c42f901d7bd49deeea75
nextcloud-deck==0.2.1 \
--hash=sha256:b1f49c8cb1833a612c2db176d1ed77ef27dde4894f61c0999cf05c9f513f8f46 \
--hash=sha256:39ec92c51e7b85081a88697f4e7d8167213b1c7782e1a60ca89169caca4c61ba
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
packaging==24.2 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759
pathspec==0.12.1 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08
platformdirs==4.3.6 \
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
pluggy==1.5.0 \
--hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
portion==2.6.0 \
--hash=sha256:6fb538b57a92058f0edd360667694448aa3fc028ab97e41e3091359d14ba4dd5 \
--hash=sha256:9132580df989b1985dae1caa3070b768f2a3360b40c89c59daa4acc7e6674dc4
pydantic-core==2.27.2 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \
--hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \
--hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \
--hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \
--hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \
--hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \
--hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \
--hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \
--hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \
--hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \
--hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \
--hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \
--hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \
--hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \
--hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \
--hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \
--hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \
--hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \
--hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \
--hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \
--hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \
--hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \
--hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \
--hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \
--hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \
--hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \
--hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \
--hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \
--hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \
--hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \
--hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \
--hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \
--hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \
--hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \
--hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \
--hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \
--hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \
--hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \
--hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \
--hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \
--hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \
--hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \
--hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \
--hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \
--hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \
--hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \
--hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \
--hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \
--hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \
--hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \
--hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \
--hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \
--hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \
--hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \
--hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \
--hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \
--hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \
--hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \
--hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \
--hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \
--hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \
--hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \
--hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \
--hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \
--hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \
--hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \
--hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \
--hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \
--hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \
--hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \
--hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \
--hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \
--hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \
--hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \
--hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \
--hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \
--hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \
--hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \
--hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \
--hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \
--hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \
--hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \
--hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \
--hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \
--hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \
--hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \
--hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \
--hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \
--hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \
--hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \
--hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \
--hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \
--hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \
--hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \
--hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \
--hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \
--hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \
--hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \
--hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \
--hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad
pydantic==2.10.6 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \
--hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236
python-dateutil==2.9.0.post0 ; python_version >= "3.12" and python_version < "4" \
pydantic==2.10.3 \
--hash=sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9 \
--hash=sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d
pydantic-core==2.27.1 \
--hash=sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235 \
--hash=sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f \
--hash=sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02 \
--hash=sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c \
--hash=sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac \
--hash=sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb \
--hash=sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529 \
--hash=sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35 \
--hash=sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089 \
--hash=sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381 \
--hash=sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb \
--hash=sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae \
--hash=sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c \
--hash=sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16 \
--hash=sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e \
--hash=sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073 \
--hash=sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08 \
--hash=sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf \
--hash=sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737 \
--hash=sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2 \
--hash=sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107 \
--hash=sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51 \
--hash=sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a \
--hash=sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc \
--hash=sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960 \
--hash=sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23 \
--hash=sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05 \
--hash=sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337 \
--hash=sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5
pydantic-settings==2.8.1 \
--hash=sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585 \
--hash=sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c
pyright==1.1.390 \
--hash=sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d \
--hash=sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110
pytest==8.3.4 \
--hash=sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761 \
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
python-dotenv==1.0.1 ; python_version >= "3.12" and python_version < "4" \
python-dotenv==1.0.1 \
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
pytz==2025.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57 \
--hash=sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e
recurring-ical-events==3.5.2 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:8622383ceb03d1b30159c2a6beb4cc3623646bfc52e349858e9315df149e52a2 \
--hash=sha256:8eb7be7a3ebd322b942fde74dbfb3c230010ab49e1d9eea1e9ea2201c1e4da93
requests==2.32.3 ; python_version >= "3.12" and python_version < "4" \
pytz==2024.2 \
--hash=sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a \
--hash=sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725
recurring-ical-events==3.3.4 \
--hash=sha256:8e7f4fb03d1a180445d618327f0698f48cd2cabf40cf37f852cb5799c8883990 \
--hash=sha256:24f8c11687e480ed90e84ee8dc8dc8920241476abe0450bd55606bac894414b1
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
six==1.17.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
sniffio==1.3.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
sortedcontainers==2.4.0 ; python_version >= "3.12" and python_version < "4" \
six==1.17.0 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
sniffio==1.3.1 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2
sortedcontainers==2.4.0 \
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
starlette==0.46.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038 \
--hash=sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50
tatsu==5.13.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:941e12df8798f444957600346c52a5ff3455f485de5b4570a3a71a00ed896a14 \
--hash=sha256:df8c216eecd09af9b5b308624f1bfda1e4b5af3d3df1bdc4f331a1e8ea6bc012
termcolor==2.5.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8 \
--hash=sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f
types-python-dateutil==2.9.0.20241206 ; python_version >= "3.12" and python_version < "4" \
starlette==0.41.3 \
--hash=sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835 \
--hash=sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7
tatsu==5.12.2 \
--hash=sha256:5894dc7ddba9a1886a95ff2f06cef1be2b3d3a37c776eba8177ef4dcd80ccb03 \
--hash=sha256:9c313186ae5262662cb3fbec52c9a12db1ef752e615f46cac3eb568cb91eacf9
termcolor==2.5.0 \
--hash=sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f \
--hash=sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8
types-python-dateutil==2.9.0.20241206 \
--hash=sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb \
--hash=sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53
typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
tzdata==2025.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694 \
--hash=sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639
urllib3==2.3.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
uvicorn==0.34.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \
--hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9
vobject==0.9.9 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:0fbdb982065cf4d1843a5d5950c88510041c6de026bda49c3502721de1c6ac3d \
--hash=sha256:ac44e5d7e2079d84c1d52c50a615b9bec4b1ba958608c4c7fe40cbf33247b38e
websockets==15.0 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:0e389efe46ccb25a1f93d08c7a74e8123a2517f7b7458f043bd7529d1a63ffeb \
--hash=sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab \
--hash=sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081 \
--hash=sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680 \
--hash=sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276 \
--hash=sha256:1657a9eecb29d7838e3b415458cc494e6d1b194f7ac73a34aa55c6fb6c72d1f3 \
--hash=sha256:17f2854c6bd9ee008c4b270f7010fe2da6c16eac5724a175e75010aacd905b31 \
--hash=sha256:190bc6ef8690cd88232a038d1b15714c258f79653abad62f7048249b09438af3 \
--hash=sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b \
--hash=sha256:24d5333a9b2343330f0f4eb88546e2c32a7f5c280f8dd7d3cc079beb0901781b \
--hash=sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c \
--hash=sha256:2bd8ef197c87afe0a9009f7a28b5dc613bfc585d329f80b7af404e766aa9e8c7 \
--hash=sha256:2ea4f210422b912ebe58ef0ad33088bc8e5c5ff9655a8822500690abc3b1232d \
--hash=sha256:30cff3ef329682b6182c01c568f551481774c476722020b8f7d0daacbed07a17 \
--hash=sha256:327adab7671f3726b0ba69be9e865bba23b37a605b585e65895c428f6e47e766 \
--hash=sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904 \
--hash=sha256:37d66646f929ae7c22c79bc73ec4074d6db45e6384500ee3e0d476daf55482a9 \
--hash=sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7 \
--hash=sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4 \
--hash=sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad \
--hash=sha256:45535fead66e873f411c1d3cf0d3e175e66f4dd83c4f59d707d5b3e4c56541c4 \
--hash=sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1 \
--hash=sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3 \
--hash=sha256:4ff380aabd7a74a42a760ee76c68826a8f417ceb6ea415bd574a035a111fd133 \
--hash=sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3 \
--hash=sha256:5294fcb410ed0a45d5d1cdedc4e51a60aab5b2b3193999028ea94afc2f554b05 \
--hash=sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d \
--hash=sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc \
--hash=sha256:5e6ee18a53dd5743e6155b8ff7e8e477c25b29b440f87f65be8165275c87fef0 \
--hash=sha256:67a04754d121ea5ca39ddedc3f77071651fb5b0bc6b973c71c515415b44ed9c5 \
--hash=sha256:7394c0b7d460569c9285fa089a429f58465db930012566c03046f9e3ab0ed181 \
--hash=sha256:789c43bf4a10cd067c24c321238e800b8b2716c863ddb2294d2fed886fa5a689 \
--hash=sha256:7ac67b542505186b3bbdaffbc303292e1ee9c8729e5d5df243c1f20f4bb9057e \
--hash=sha256:8561c48b0090993e3b2a54db480cab1d23eb2c5735067213bb90f402806339f5 \
--hash=sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72 \
--hash=sha256:8711682a629bbcaf492f5e0af72d378e976ea1d127a2d47584fa1c2c080b436b \
--hash=sha256:89da58e4005e153b03fe8b8794330e3f6a9774ee9e1c3bd5bc52eb098c3b0c4f \
--hash=sha256:89f72524033abbfde880ad338fd3c2c16e31ae232323ebdfbc745cbb1b3dcc03 \
--hash=sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f \
--hash=sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37 \
--hash=sha256:94c4a9b01eede952442c088d415861b0cf2053cbd696b863f6d5022d4e4e2453 \
--hash=sha256:98dcf978d4c6048965d1762abd534c9d53bae981a035bfe486690ba11f49bbbb \
--hash=sha256:a4cc73a6ae0a6751b76e69cece9d0311f054da9b22df6a12f2c53111735657c8 \
--hash=sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc \
--hash=sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55 \
--hash=sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8 \
--hash=sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99 \
--hash=sha256:b499caef4bca9cbd0bd23cd3386f5113ee7378094a3cb613a2fa543260fe9506 \
--hash=sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72 \
--hash=sha256:bd66b4865c8b853b8cca7379afb692fc7f52cf898786537dfb5e5e2d64f0a47f \
--hash=sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6 \
--hash=sha256:c24ba103ecf45861e2e1f933d40b2d93f5d52d8228870c3e7bf1299cd1cb8ff1 \
--hash=sha256:c348abc5924caa02a62896300e32ea80a81521f91d6db2e853e6b1994017c9f6 \
--hash=sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9 \
--hash=sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596 \
--hash=sha256:c86dc2068f1c5ca2065aca34f257bbf4f78caf566eb230f692ad347da191f0a1 \
--hash=sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375 \
--hash=sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab \
--hash=sha256:cc8821a03bcfb36e4e4705316f6b66af28450357af8a575dc8f4b09bf02a3dee \
--hash=sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f \
--hash=sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1 \
--hash=sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d \
--hash=sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965 \
--hash=sha256:e413352a921f5ad5d66f9e2869b977e88d5103fc528b6deb8423028a2befd842 \
--hash=sha256:ee06405ea2e67366a661ed313e14cf2a86e84142a3462852eb96348f7219cee3 \
--hash=sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7 \
--hash=sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3 \
--hash=sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa \
--hash=sha256:ffc5ae23ada6515f31604f700009e2df90b091b67d463a8401c1d8a37f76c1d7
x-wr-timezone==2.0.1 ; python_version >= "3.12" and python_version < "4" \
--hash=sha256:9166c40e6ffd4c0edebabc354e1a1e2cffc1bb473f88007694793757685cc8c3 \
--hash=sha256:e74a53b9f4f7def8138455c240e65e47c224778bce3c024fcd6da2cbe91ca038
typing-extensions==4.12.2 \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d
tzdata==2024.2 \
--hash=sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc \
--hash=sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd
urllib3==2.2.3 \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac
vobject==0.9.9 \
--hash=sha256:ac44e5d7e2079d84c1d52c50a615b9bec4b1ba958608c4c7fe40cbf33247b38e \
--hash=sha256:0fbdb982065cf4d1843a5d5950c88510041c6de026bda49c3502721de1c6ac3d
watchfiles==1.0.4 \
--hash=sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205 \
--hash=sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2 \
--hash=sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9 \
--hash=sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712 \
--hash=sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12 \
--hash=sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844 \
--hash=sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733 \
--hash=sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af \
--hash=sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a \
--hash=sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff \
--hash=sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e \
--hash=sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94 \
--hash=sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c \
--hash=sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90 \
--hash=sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9 \
--hash=sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60 \
--hash=sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407 \
--hash=sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d \
--hash=sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d \
--hash=sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b \
--hash=sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590 \
--hash=sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902 \
--hash=sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1 \
--hash=sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303 \
--hash=sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80 \
--hash=sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc
websockets==14.1 \
--hash=sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8 \
--hash=sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed \
--hash=sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d \
--hash=sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707 \
--hash=sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a \
--hash=sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45 \
--hash=sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58 \
--hash=sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058 \
--hash=sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4 \
--hash=sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05 \
--hash=sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0 \
--hash=sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f \
--hash=sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9 \
--hash=sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b \
--hash=sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3 \
--hash=sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59 \
--hash=sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2 \
--hash=sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da \
--hash=sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9 \
--hash=sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7 \
--hash=sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a \
--hash=sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6 \
--hash=sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0 \
--hash=sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e
x-wr-timezone==2.0.0 \
--hash=sha256:1e475d2dedcd2128550cd88b4b5773a224e936e1c9f22ff50104622180455265 \
--hash=sha256:37f3927ddc32970c330af97773dc12bf8e0d2deb9ede5c3c676dc13ed62feb11
+4
View File
@@ -0,0 +1,4 @@
import requests
import os
print(requests.get("http://localhost:" + os.environ["GRANIAN_PORT"] + "/live"))
+67 -8
View File
@@ -10,6 +10,9 @@ from fastapi.websockets import WebSocket
client = TestClient(app)
BOARD_NAME = "Infra"
BOARD_STACK = "Doing"
def test_websocket():
with client.websocket_connect("/ws/test-client") as websocket:
@@ -18,13 +21,6 @@ def test_websocket():
assert "Client #test-client dit: Hello WebSocket" in data
def test_echo_message():
test_message = {"text": "Hello FastAPI!"}
response = client.post("/echo", json=test_message)
assert response.status_code == 200
assert response.json() == {"message": test_message["text"]}
def test_websocket_echo():
with client.websocket_connect("/ws/test-client") as websocket:
websocket.send_text("Hello WebSocket")
@@ -32,4 +28,67 @@ def test_websocket_echo():
assert response == "Client #test-client dit: Hello WebSocket"
def create_event_and_delete():
def test_healthcheck():
response = client.get("/live")
assert response.status_code == 200
assert response.json() == {"message": "OK"}
def test_get_boards():
response = client.get("/boards")
assert response.status_code == 200
# assert response.json() == {"message": "OK"}
def test_get_board():
response = client.get(f"/board/{BOARD_NAME}")
assert response.status_code == 200
def test_get_cards():
response = client.get(f"/cards/{BOARD_NAME}")
assert response.status_code == 200
def test_get_stack():
response = client.get(f"/board/{BOARD_NAME}/stack/{BOARD_STACK}")
assert response.status_code == 200
# def test_get_stack_by_id():
# response = client.get(f"/board/{BOARD_NAME}/stack/{BOARD_STACK}")
# assert response.status_code == 200
def test_get_labels():
response = client.get(f"/board/{BOARD_NAME}/labels")
assert response.status_code == 200
def test_get_events():
response = client.get(f"/events")
assert response.status_code == 200
assert response.json()
def test_get_gaps():
response = client.get(f"/gaps")
assert response.status_code == 200
def test_get_gaps_2days():
response = client.get(f"/gaps/2")
assert response.status_code == 200
def test_sort_by_priority():
response = client.get(f"/sort-by-priority/{BOARD_NAME}")
assert response.status_code == 200
def test_get_top_priority():
response = client.get(f"/top-priority/{BOARD_NAME}")
assert response.status_code == 200
# def create_event_and_delete():
Generated
+92 -17
View File
@@ -194,6 +194,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 },
]
[[package]]
name = "granian"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/18/58f706aa9f79bd31dd68ac144e88c2a82afcc1aab84f33947559c8a7bc37/granian-2.0.0.tar.gz", hash = "sha256:d040c63e069ce195607530afa53fc1fd50b8e99284184f051e5bd6b4c3742081", size = 92342 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/25/dec6bf5fa6bac1ae4ecdba3a0097065faf80f3bf2032944bd2b0f79e2c10/granian-2.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:16b71f2e5d0c7e2059cc5902dc61908c4dbb54088c09acc14457c89f1778936d", size = 2864444 },
{ url = "https://files.pythonhosted.org/packages/fb/f7/7934eb3552eedeb918f6f59c6e9f4334f83d358befa743eaf62a08ded7d3/granian-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c58be99fb385ee219111cdaf6f5b069c668a3345551b4209073a1db83125b94", size = 2640479 },
{ url = "https://files.pythonhosted.org/packages/d1/3f/ea56204a5e7f89f4de3b1f16ee51c5c538f92c864af1736293442343dad7/granian-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f4695bcf6e2c3842e212dc42253be0400ee68e7b2245555d9d04ffb44e85919", size = 2999510 },
{ url = "https://files.pythonhosted.org/packages/7c/3d/a3e3ca28fcc9a7d4ac23c95535eb3bc25d1b174227525069d9e05ed4c82b/granian-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7122062532355af2c87048f859f6006a452073af5e88dd4b0b8f8eafb4107b5", size = 3071416 },
{ url = "https://files.pythonhosted.org/packages/6d/8b/9865bfd80fc06ec249347b3a0e89a6059870a8e235591bda3ed3f502a92d/granian-2.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0618855fcf40a0c768839031f5c55cf0debe0b4fc123c129e5a6f0166e341fc5", size = 3000238 },
{ url = "https://files.pythonhosted.org/packages/e9/2c/72337748b7f9b06adff13a31ae6559b960da54398d533fbb64d83c390899/granian-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6a2d7c51d20cb0fe831d481c980f4ff2c400a4c7f84abad66d8c1ac94f468921", size = 3109544 },
{ url = "https://files.pythonhosted.org/packages/57/66/678f01ccdfb32c58d9d89797982c363110e33049929b874f2b2d4c40b7b7/granian-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:54bf94c4bbfb2630b33b1a075b4571fa2b1e870cbf06f4145c29e13122aec8ff", size = 2609526 },
{ url = "https://files.pythonhosted.org/packages/3a/16/992b68e535313103e66f8667d3af0f68362f85131d192239ff304418ca3a/granian-2.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f98a6d604b0dbdd226b0a0c823242706bd99070abd6977d4903c4249d231aab4", size = 2863592 },
{ url = "https://files.pythonhosted.org/packages/d8/b4/5d1d055512b398c0423100a3a07a9a1cf3af4558aba207f05dfd2e2fa8a6/granian-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f83644dace8acb07a4bd36c5fdf6731eea785ef243a8c5ee77361eec09c2c35", size = 2640150 },
{ url = "https://files.pythonhosted.org/packages/5d/64/0edb5188db312ed32037626adefb042a4a08d728b67657fe9295dd80798a/granian-2.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f65c7d4b6d5280a622a3e0ed899654221d748cf7bde801d701f2d7266ec0cb8e", size = 3002636 },
{ url = "https://files.pythonhosted.org/packages/97/96/6afac3a929a45ceed1b2317bfa48ea425607ed5badf82e5aee0bd771302d/granian-2.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87b851f38e1cf3d48bb49069efb7e4b18db46c664512e3dd6d0fea5ce6806e1", size = 3070948 },
{ url = "https://files.pythonhosted.org/packages/7a/8b/f49b53e321d3786aa5f3b06edd7083253e4f809dd6e1bc0d3bcf21bea8f5/granian-2.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a71872e7f619f7b8e543fdfcdb0a5025395cd36326f53558474f4c9c243219de", size = 3000279 },
{ url = "https://files.pythonhosted.org/packages/40/54/447887eacc6b359384d992783f9504606d838004f6a53c1292b10f8ee4f3/granian-2.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:70cdb93c63711f3da3e4b3a465affb514cb933635fdbdbfd0349c4f2920069ab", size = 3109187 },
{ url = "https://files.pythonhosted.org/packages/6b/de/b0b98e19839b54b9dfd873984b3535ff73ec8cf5d0fe02d5a027a2e8333c/granian-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d3fc2895de84a15ded59ecc69a100ff5e53cfb6ac9eb96f6db00d571b604b1c", size = 2609375 },
{ url = "https://files.pythonhosted.org/packages/98/13/6a5aaf474640c9129a2b7715bcbf9a779693d97833220f47dc566629baca/granian-2.0.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:565c90932189b759534096d260837a6af3dac492490fa4858339f5d251181e94", size = 2813729 },
{ url = "https://files.pythonhosted.org/packages/35/41/29271bd7707c40d5630514ff8e41d58ae9e230d0f0f50321a61910825577/granian-2.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5769cf26bd27bdfce2932570ec3cf1e6703b493ef9d64acbba7740800299982a", size = 2556192 },
{ url = "https://files.pythonhosted.org/packages/49/5d/415fbe74ffcf0a290aa616082bbdbc45182f1350429d3ebdab8f0b83aea1/granian-2.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88be218f5daad58b2984aeec27720b35082d47bbb1cd1b6f910168431a94c181", size = 2832816 },
{ url = "https://files.pythonhosted.org/packages/9b/e2/7002c59900f6c433bda4d9598a7f2920d1f4248f20f866240601cde75400/granian-2.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8ce3356f8016aababf946caec88d3423102d428e6c988538de83e1345d419a", size = 2922955 },
{ url = "https://files.pythonhosted.org/packages/c6/1b/55f5a17ce60e552a5828819e6b07e330f8118225d6db7f118e2beb928ca5/granian-2.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:1598cc112b15b78915a3231d916c1db14c6d00e1b3802118a9610ac4e171dea1", size = 2991128 },
{ url = "https://files.pythonhosted.org/packages/ff/e5/4eab530da2ce4b38e745dde3a5401c5419deb24e2930202244ae1f11c82d/granian-2.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:035f765b7a918c8b197917b3451dd4653db3478b9c8221c2a7f0532472a203a9", size = 3096132 },
{ url = "https://files.pythonhosted.org/packages/79/87/e4f94b4ae21d81bf258358bac0c173cf830aede6aa679627eae538114196/granian-2.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b80e1acdd3ec632abec6ddce9c549351ecf0bd7a24914f338920375a28e35f8b", size = 2612935 },
]
[package.optional-dependencies]
reload = [
{ name = "watchfiles" },
]
[[package]]
name = "h11"
version = "0.14.0"
@@ -467,6 +504,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pyright"
version = "1.1.390"
@@ -619,20 +669,21 @@ source = { virtual = "." }
dependencies = [
{ name = "caldav" },
{ name = "fastapi" },
{ name = "icalendar" },
{ name = "granian" },
{ name = "ics" },
{ name = "nextcloud-api-wrapper" },
{ name = "nextcloud-deck" },
{ name = "portion" },
{ name = "pydantic-settings" },
{ name = "python-dotenv" },
{ name = "termcolor" },
{ name = "uvicorn" },
{ name = "websockets" },
]
[package.dev-dependencies]
dev = [
{ name = "black" },
{ name = "granian", extra = ["reload"] },
{ name = "httpx" },
{ name = "pyright" },
{ name = "pytest" },
@@ -642,20 +693,21 @@ dev = [
requires-dist = [
{ name = "caldav", specifier = ">=1.4.0" },
{ name = "fastapi", specifier = ">=0.115.6" },
{ name = "icalendar", specifier = ">=6.1.0" },
{ name = "granian", specifier = ">=2.0.0" },
{ name = "ics", specifier = ">=0.7.2" },
{ name = "nextcloud-api-wrapper", specifier = ">=0.2.3" },
{ name = "nextcloud-deck", specifier = ">=0.2.1" },
{ name = "portion", specifier = ">=2.6.0,<3.0.0" },
{ name = "pydantic-settings", specifier = ">=2.8.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "termcolor", specifier = ">=2.5.0" },
{ name = "uvicorn", specifier = ">=0.34.0" },
{ name = "websockets", specifier = ">=14.1" },
]
[package.metadata.requires-dev]
dev = [
{ name = "black", specifier = ">=24.10.0" },
{ name = "granian", extras = ["reload"], specifier = ">=2.0.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pyright", specifier = ">=1.1.390" },
{ name = "pytest", specifier = ">=8.3.4" },
@@ -697,19 +749,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]
[[package]]
name = "vobject"
version = "0.9.9"
@@ -724,6 +763,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/20/6bba813bbd498c28edbbcf8253a6398cf4266ecf7bfa6129835c0a2bfbb1/vobject-0.9.9-py2.py3-none-any.whl", hash = "sha256:0fbdb982065cf4d1843a5d5950c88510041c6de026bda49c3502721de1c6ac3d", size = 47526 },
]
[[package]]
name = "watchfiles"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
{ url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
{ url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
{ url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
{ url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
{ url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
{ url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
{ url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
{ url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
{ url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
{ url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
{ url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
{ url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
{ url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
{ url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
{ url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
{ url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
{ url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
{ url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
{ url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
]
[[package]]
name = "websockets"
version = "14.1"