diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..7e49bff Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b0adae..38ac99d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,18 +12,13 @@ jobs: id-token: write steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.11" - - name: Install dependencies - run: | - curl -sSL https://install.python-poetry.org | python3 - - python -m pip install --upgrade pip - pip install setuptools wheel twine - poetry install - - name: Build and publish - run: | - poetry build + enable-cache: true + - name: Set up Python + run: uv python install + - name: Build package + run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6c7a000..4df829d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,17 +11,14 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@v4 - - name: Set up Python + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: "Set up Python" uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies - run: | - curl -sSL https://install.python-poetry.org | python3 - - python -m pip install --upgrade pip - pip install setuptools wheel twine - poetry install + run: uv sync - name: Run tests run: | - poetry run pytest + uv run pytest diff --git a/.gitignore b/.gitignore index 7f50f6c..080a1c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__/ .DS_Store .env -.idea/ \ No newline at end of file +.idea/ +.venv/ +.pytest_cache/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..92536a9 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.0 diff --git a/pyproject.toml b/pyproject.toml index 4425487..18467c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,62 @@ -[tool.poetry] -name = "python-motion" -version = "0.2.1" -description = "" -authors = ["Lemuel Boyce "] +[project] +name = "motion" +dynamic = ["version"] +urls = { Homepage = "https://github.com/rhymiz/python-motion", GitHub = "https://github.com/rhymiz/python-motion" } +description = "A Python library for interfacing with Motion (usemotion.com) API" readme = "README.md" license = "MIT" -packages = [{ "include" = "motion", from = "src" }] +authors = [{ name = "Lemuel Boyce", email = "lemuel@vokality.com" }] +keywords = [ + "motion", + "usemotion", + "api", + "client", + "usemotion.com", + "productivity", + "ai", +] +requires-python = ">=3.11" +dependencies = ["pydantic>=2.11.5", "requests>=2.32.3"] +classifiers = [ + "Topic :: Software Development :: Libraries", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] -[tool.poetry.dependencies] -python = "^3.11" -requests = "^2.32.0" -pydantic = "^2.9.2" -[tool.poetry.group.dev.dependencies] -ruff = "*" -ruff-lsp = "*" -pytest = "*" +[dependency-groups] +dev = [ + "pyright>=1.1.401", + "pytest>=8.3.5", + "pytest-cov>=6.1.1", + "ruff>=0.11.12", +] + [tool.ruff] +exclude = [".venv"] line-length = 79 indent-width = 4 -target-version = "py311" +target-version = "py312" +respect-gitignore = true + +[tool.ruff.lint] +select = ["E", "I", "B", "ASYNC"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["D"] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "parents" + +[tool.ruff.analyze] +detect-string-imports = true + [tool.ruff.format] quote-style = "double" @@ -30,6 +66,18 @@ line-ending = "auto" docstring-code-format = true docstring-code-line-length = 79 +[tool.pyright] +exclude = [".venv", "tests"] +typeCheckingMode = "strict" + + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + + [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/src/motion/__init__.py b/src/motion/__init__.py index 340928c..f2d45bc 100644 --- a/src/motion/__init__.py +++ b/src/motion/__init__.py @@ -4,6 +4,7 @@ ProjectResource, RecurringTaskResource, ScheduleResource, + StatusResource, TaskResource, UserResource, WorkspaceResource, @@ -23,6 +24,7 @@ def __init__(self, api_key: str) -> None: self.comments = CommentResource(self._client) self.workspaces = WorkspaceResource(self._client) self.schedules = ScheduleResource(self._client) + self.statuses = StatusResource(self._client) self.recurring_tasks = RecurringTaskResource(self._client) diff --git a/src/motion/client.py b/src/motion/client.py index ca41468..f794bd6 100644 --- a/src/motion/client.py +++ b/src/motion/client.py @@ -30,7 +30,7 @@ def call_api( self, method: HttpMethod, path: str, - data: GenericTypedDict | None = None, + data: GenericTypedDict[Any] | None = None, params: dict[str, Any] | None = None, ) -> Response: """ diff --git a/src/motion/models/__init__.py b/src/motion/models/__init__.py index e69c181..11e76b3 100644 --- a/src/motion/models/__init__.py +++ b/src/motion/models/__init__.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union from pydantic import BaseModel, Field @@ -66,6 +66,20 @@ class ListProjects(BaseModel): meta: Optional[MetaResult] = None +# ------------------------------------------------------------- +# Response / Domain Models +# ------------------------------------------------------------- + +# NOTE: The following models aim to mirror the schemas defined in the +# `Motion REST API swagger.json` file located under the `data/` +# directory. Where the OpenAPI specification marks a field as +# "required" the attribute is defined without a default (or with +# an explicit `Field(...)`) even when the logical value might be +# `None`/`null` in responses. This guarantees that model +# validation will fail if the API omits a required key, keeping +# the SDK completely aligned with the contract. + + class RecurringTask(BaseModel): workspace: Workspace id: str @@ -75,7 +89,9 @@ class RecurringTask(BaseModel): assignee: User project: Optional[Project] = None status: Status - priority: str + # According to the schema the priority field in the response can be + # one of the task-level priorities (ASAP/HIGH/MEDIUM/LOW). + priority: Literal["ASAP", "HIGH", "MEDIUM", "LOW"] labels: List[Label] @@ -85,47 +101,100 @@ class ListRecurringTasks(BaseModel): class DailySchedule(BaseModel): - start: str - end: str + start: str = Field(..., description="24 hour time format. HH:mm") + end: str = Field(..., description="24 hour time format. HH:mm") class ScheduleBreakout(BaseModel): - monday: List[DailySchedule] - tuesday: List[DailySchedule] - wednesday: List[DailySchedule] - thursday: List[DailySchedule] - friday: List[DailySchedule] - saturday: List[DailySchedule] - sunday: List[DailySchedule] + monday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + tuesday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + wednesday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + thursday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + friday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + saturday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) + sunday: List[DailySchedule] = Field( + ..., + description="Array could be empty if there is no range for this day", + ) class Schedule(BaseModel): name: str isDefaultTimezone: bool timezone: str - schedule: ScheduleBreakout + schedule: ScheduleBreakout = Field( + ..., + description="Schedule broken out by day. It is possible for a day to have more than one start/end time", + ) + + +class AutoScheduledInfo(BaseModel): + startDate: Optional[datetime] = Field( + None, + description="ISO 8601 Date which is trimmed to the start of the day passed", + ) + deadlineType: Literal["HARD", "SOFT", "NONE"] = Field(default="SOFT") + schedule: str = Field( + default="Work Hours", + description="Schedule the task must adhere to. Schedule MUST be 'Work Hours' if scheduling the task for another user.", + ) class Task(BaseModel): - duration: Union[str, int] + duration: Union[Literal["NONE", "REMINDER"], int] = Field( + default=30, + description='A duration can be one of the following... "NONE", "REMINDER", or a integer greater than 0', + ) workspace: Workspace id: str name: str description: Optional[str] = None dueDate: datetime - deadlineType: str - parentRecurringTaskId: Optional[str] = None + deadlineType: Literal["HARD", "SOFT", "NONE"] = Field(default="SOFT") + # The schema marks this field as required but allows it to be + # `null` when the task is not generated from a recurring template. + parentRecurringTaskId: Optional[str] = Field( + ..., description="The id of the recurring task this task belongs to if any" + ) completed: bool creator: User project: Optional[Project] = None status: Status - priority: str + priority: Literal["ASAP", "HIGH", "MEDIUM", "LOW"] labels: List[Label] assignees: List[User] - scheduledStart: Optional[datetime] = None - createdTime: datetime - scheduledEnd: Optional[datetime] = None - schedulingIssue: bool + scheduledStart: Optional[datetime] = Field( + None, + description="The time that motion has scheduled this task to start", + ) + createdTime: datetime = Field( + ..., description="The time that the task was created" + ) + scheduledEnd: Optional[datetime] = Field( + None, description="The time that motion has scheduled this task to end" + ) + schedulingIssue: bool = Field( + ..., + description="Returns true if Motion was unable to schedule this task. Check Motion directly to address", + ) class ListTasks(BaseModel): @@ -141,3 +210,124 @@ class ListUsers(BaseModel): class ListWorkspaces(BaseModel): workspaces: List[Workspace] meta: Optional[MetaResult] = None + + +# Request/Post Models +class CommentPost(BaseModel): + taskId: str + content: str + + +class ProjectPost(BaseModel): + dueDate: Optional[datetime] = Field( + None, description="ISO 8601 Due date on the task" + ) + name: str = Field(..., min_length=1) + workspaceId: str + description: Optional[str] = None + labels: Optional[List[str]] = None + status: Optional[str] = None + priority: Literal["ASAP", "HIGH", "MEDIUM", "LOW"] = Field( + default="MEDIUM" + ) + + +class RecurringTasksPost(BaseModel): + frequency: str = Field( + ..., + description="Frequency in which the task should be scheduled. Please carefully read how to construct above.", + ) + deadlineType: Literal["HARD", "SOFT"] = Field(default="SOFT") + duration: Union[Literal["REMINDER"], int] = Field( + default=30, + description='A duration can be one of the following... "REMINDER", or a integer greater than 0', + ) + startingOn: Optional[datetime] = Field( + default=None, + description="ISO 8601 Date which is trimmed to the start of the day passed", + ) + idealTime: Optional[str] = None + schedule: str = Field( + default="Work Hours", description="Schedule the task must adhere to" + ) + name: str = Field( + ..., min_length=1, description="Name / title of the task" + ) + workspaceId: str + description: Optional[str] = None + priority: Literal["HIGH", "MEDIUM"] = Field(default="MEDIUM") + assigneeId: str = Field( + ..., description="The user id the task should be assigned too" + ) + + +class TaskPost(BaseModel): + dueDate: Optional[datetime] = Field( + None, + description="ISO 8601 Due date on the task. REQUIRED for scheduled tasks", + ) + duration: Union[Literal["NONE", "REMINDER"], int] = Field( + default=30, + description='A duration can be one of the following... "NONE", "REMINDER", or a integer greater than 0', + ) + status: Optional[str] = Field( + None, description="Defaults to workspace default status." + ) + autoScheduled: Optional[AutoScheduledInfo] = Field( + None, + description="Set values to turn auto scheduling on, set value to null if you want to turn auto scheduling off. The status for the task must have auto scheduling enabled.", + ) + name: str = Field( + ..., min_length=1, description="Name / title of the task" + ) + projectId: Optional[str] = None + workspaceId: str + description: Optional[str] = Field( + None, description="Input as GitHub Flavored Markdown" + ) + priority: Literal["ASAP", "HIGH", "MEDIUM", "LOW"] = Field( + default="MEDIUM" + ) + labels: Optional[List[str]] = None + assigneeId: Optional[str] = Field( + None, description="The user id the task should be assigned to" + ) + + +class TaskPatch(BaseModel): + name: Optional[str] = Field( + None, min_length=1, description="Name / title of the task" + ) + dueDate: Optional[datetime] = Field( + None, + description="ISO 8601 Due date on the task. REQUIRED for scheduled tasks", + ) + assigneeId: Optional[str] = Field( + None, + description="The user id the task should be assigned to, setting the value to null will remove the assignee", + ) + duration: Optional[Union[Literal["NONE", "REMINDER"], int]] = Field( + None, + description='A duration can be one of the following... "NONE", "REMINDER", or a integer greater than 0', + ) + status: Optional[str] = Field( + None, description="Defaults to workspace default status." + ) + autoScheduled: Optional[AutoScheduledInfo] = Field( + None, + description="Set values to turn auto scheduling on, set value to null if you want to turn auto scheduling off. The status for the task must have auto scheduling enabled.", + ) + projectId: Optional[str] = None + description: Optional[str] = Field( + None, description="Input as GitHub Flavored Markdown" + ) + priority: Optional[Literal["ASAP", "HIGH", "MEDIUM", "LOW"]] = None + labels: Optional[List[str]] = None + + +class MoveTask(BaseModel): + workspaceId: str + assigneeId: Optional[str] = Field( + None, + description="The user id the task should be assigned to. Optional according to the OpenAPI specification.", + ) diff --git a/src/motion/py.typed b/src/motion/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/motion/resources/__init__.py b/src/motion/resources/__init__.py index 62d884c..32a25d5 100644 --- a/src/motion/resources/__init__.py +++ b/src/motion/resources/__init__.py @@ -1,9 +1,10 @@ -from .user import UserResource -from .task import TaskResource, RecurringTaskResource -from .project import ProjectResource from .comment import CommentResource -from .workspace import WorkspaceResource +from .project import ProjectResource from .schedule import ScheduleResource +from .status import StatusResource +from .task import RecurringTaskResource, TaskResource +from .user import UserResource +from .workspace import WorkspaceResource __all__ = [ "UserResource", @@ -13,4 +14,5 @@ "CommentResource", "WorkspaceResource", "ScheduleResource", + "StatusResource", ] diff --git a/src/motion/resources/base.py b/src/motion/resources/base.py index 7fa29d2..d787a6c 100644 --- a/src/motion/resources/base.py +++ b/src/motion/resources/base.py @@ -1,13 +1,23 @@ from abc import ABC +from typing import Any, Generic, List, TypeVar -from requests import Response +from pydantic import BaseModel from ..client import GenericTypedDict, HttpClient, HttpMethod +# Type variables with proper bounds +CreateDataT = TypeVar("CreateDataT", bound=GenericTypedDict[Any]) +UpdateDataT = TypeVar("UpdateDataT", bound=GenericTypedDict[Any]) +ListParamsT = TypeVar("ListParamsT", bound=GenericTypedDict[Any]) +ModelT = TypeVar("ModelT", bound=BaseModel) +ListModelT = TypeVar("ListModelT", bound=BaseModel) -class Resource(ABC): + +class Resource( + ABC, Generic[CreateDataT, UpdateDataT, ListParamsT, ModelT, ListModelT] +): """ - Base class for Motion API resources + Base class for Motion API resources that return wrapped responses """ base_path: str @@ -15,39 +25,80 @@ class Resource(ABC): def __init__(self, client: HttpClient) -> None: self._client = client - assert ( - self.base_path is not None - ), "base_path must be defined on resource" + assert self.base_path is not None, ( + "base_path must be defined on resource" + ) - def create(self, data: GenericTypedDict) -> Response: - return self._client.call_api( + def create(self, data: CreateDataT) -> ModelT: + response = self._client.call_api( HttpMethod.POST, self.base_path, data=data, ) + return self._parse_model(response.json()) - def update(self, object_id: str, data: GenericTypedDict) -> Response: - return self._client.call_api( + def update(self, object_id: str, data: UpdateDataT) -> ModelT: + response = self._client.call_api( HttpMethod.PUT, - path=f"{self.base_path}/{object_id}", + f"{self.base_path}/{object_id}", data=data, ) + return self._parse_model(response.json()) - def delete(self, object_id: str) -> Response: - return self._client.call_api( + def delete(self, object_id: str) -> None: + self._client.call_api( HttpMethod.DELETE, - path=f"{self.base_path}/{object_id}", + f"{self.base_path}/{object_id}", ) - def list(self, params: GenericTypedDict | None = None) -> Response: - return self._client.call_api( + def list(self, params: ListParamsT | None = None) -> ListModelT: + response = self._client.call_api( HttpMethod.GET, - path=self.base_path, + self.base_path, params=params, # type: ignore ) + return self._parse_list_model(response.json()) - def retrieve(self, object_id: str) -> Response: - return self._client.call_api( + def retrieve(self, object_id: str) -> ModelT: + response = self._client.call_api( HttpMethod.GET, - path=f"{self.base_path}/{object_id}", + f"{self.base_path}/{object_id}", + ) + return self._parse_model(response.json()) + + def _parse_model(self, data: Any) -> ModelT: + """Override in subclasses to parse single model response""" + raise NotImplementedError("Subclasses must implement _parse_model") + + def _parse_list_model(self, data: Any) -> ListModelT: + """Override in subclasses to parse list model response""" + raise NotImplementedError( + "Subclasses must implement _parse_list_model" ) + + +class SimpleListResource(ABC, Generic[ListParamsT, ModelT]): + """ + Resource class for endpoints that return raw arrays instead of wrapped objects + """ + + base_path: str + + def __init__(self, client: HttpClient) -> None: + self._client = client + + assert self.base_path is not None, ( + "base_path must be defined on resource" + ) + + def list(self, params: ListParamsT | None = None) -> List[ModelT]: + response = self._client.call_api( + HttpMethod.GET, + self.base_path, + params=params, # type: ignore + ) + return self._parse_list(response.json()) + + def _parse_list(self, data: Any) -> List[ModelT]: + """Override in subclasses to parse array response""" + raise NotImplementedError("Subclasses must implement _parse_list") diff --git a/src/motion/resources/comment.py b/src/motion/resources/comment.py index 14402ad..f3575b6 100644 --- a/src/motion/resources/comment.py +++ b/src/motion/resources/comment.py @@ -1,26 +1,33 @@ -from typing import TypedDict +from typing import Any, NotRequired, Required +from ..client import GenericTypedDict from ..models import Comment, ListComments from .base import Resource -class CommentCreate(TypedDict): - taskId: str - content: str +class CommentCreateData(GenericTypedDict[Any]): + taskId: Required[str] + content: Required[str] -class CommentListParams(TypedDict, total=False): - cursor: str - taskId: str +class CommentListParams(GenericTypedDict[Any]): + taskId: Required[str] + cursor: NotRequired[str] -class CommentResource(Resource): +class CommentResource( + Resource[ + CommentCreateData, + CommentCreateData, + CommentListParams, + Comment, + ListComments, + ] +): base_path = "/comments" - def create(self, data: CommentCreate) -> Comment: - response = super().create(data) - return Comment.model_validate(response.json()) + def _parse_model(self, data: Any) -> Comment: + return Comment.model_validate(data) - def list(self, params: CommentListParams | None = None) -> ListComments: - response = super().list(params) - return ListComments.model_validate(response.json()) + def _parse_list_model(self, data: Any) -> ListComments: + return ListComments.model_validate(data) diff --git a/src/motion/resources/project.py b/src/motion/resources/project.py index 203f2c0..c29d941 100644 --- a/src/motion/resources/project.py +++ b/src/motion/resources/project.py @@ -1,31 +1,35 @@ -from typing import TypedDict, List +from typing import Any, List, Literal, NotRequired, Required + +from ..client import GenericTypedDict +from ..models import ListProjects, Project from .base import Resource -from ..models import Project, ListProjects -class ProjectCreate(TypedDict): - name: str - workspaceId: str - description: str | None - labels: List[str] | None - status: str | None - priority: str +class ProjectCreate(GenericTypedDict[Any]): + name: Required[str] + workspaceId: Required[str] + priority: Required[Literal["ASAP", "HIGH", "MEDIUM", "LOW"]] -class ProjectListParams(TypedDict, total=False): - cursor: str - workspaceId: str + dueDate: NotRequired[str] + description: NotRequired[str] + labels: NotRequired[List[str]] + status: NotRequired[str] -class ProjectResource(Resource): - base_path = "/projects" - def create(self, data: ProjectCreate) -> Project: - response = super().create(data) - return Project.model_validate(response.json()) +class ProjectListParams(GenericTypedDict[Any]): + workspaceId: Required[str] + cursor: NotRequired[str] + + +class ProjectResource( + Resource[ + ProjectCreate, ProjectCreate, ProjectListParams, Project, ListProjects + ] +): + base_path = "/projects" - def list(self, params: ProjectListParams | None = None) -> ListProjects: - response = super().list(params) - return ListProjects.model_validate(response.json()) + def _parse_model(self, data: Any) -> Project: + return Project.model_validate(data) - def retrieve(self, object_id: str) -> Project: - response = super().retrieve(object_id) - return Project.model_validate(response.json()) + def _parse_list_model(self, data: Any) -> ListProjects: + return ListProjects.model_validate(data) diff --git a/src/motion/resources/schedule.py b/src/motion/resources/schedule.py index d65fe3f..326af9b 100644 --- a/src/motion/resources/schedule.py +++ b/src/motion/resources/schedule.py @@ -1,10 +1,13 @@ -from typing import List -from .base import Resource +from typing import Any, List + +from ..client import GenericTypedDict from ..models import Schedule +from .base import SimpleListResource + -class ScheduleResource(Resource): +class ScheduleResource(SimpleListResource[GenericTypedDict[Any], Schedule]): base_path = "/schedules" - def list(self) -> List[Schedule]: - response = super().list() - return [Schedule.model_validate(item) for item in response.json()] + def _parse_list(self, data: Any) -> List[Schedule]: + # The API returns a raw array, not a wrapped object + return [Schedule.model_validate(item) for item in data] diff --git a/src/motion/resources/status.py b/src/motion/resources/status.py new file mode 100644 index 0000000..a2bbe19 --- /dev/null +++ b/src/motion/resources/status.py @@ -0,0 +1,16 @@ +from typing import Any, List, Required + +from ..client import GenericTypedDict +from ..models import Status +from .base import SimpleListResource + + +class StatusListParams(GenericTypedDict[Any]): + workspaceId: Required[str] + + +class StatusResource(SimpleListResource[StatusListParams, Status]): + base_path = "/statuses" + + def _parse_list(self, data: Any) -> List[Status]: + return [Status.model_validate(item) for item in data] diff --git a/src/motion/resources/task.py b/src/motion/resources/task.py index 1b4b733..6142e7e 100644 --- a/src/motion/resources/task.py +++ b/src/motion/resources/task.py @@ -1,118 +1,139 @@ -from typing import Any, List, TypedDict +from typing import Any, List, Literal, NotRequired, Required, Union -from ..client import HttpMethod +from ..client import GenericTypedDict, HttpMethod from ..models import ListRecurringTasks, ListTasks, RecurringTask, Task from .base import Resource -class TaskCreate(TypedDict, total=False): - name: str - workspaceId: str - dueDate: str - duration: int | str - status: str - autoScheduled: dict[str, Any] | None - projectId: str | None - description: str | None - priority: str - labels: List[str] | None - assigneeId: str | None - - -class TaskUpdate(TypedDict, total=False): - name: str - dueDate: str - assigneeId: str | None - duration: int | str - status: str - autoScheduled: dict[str, Any] | None - projectId: str - description: str - priority: str - labels: List[str] - - -class TaskListParams(TypedDict, total=False): - cursor: str - label: str - status: List[str] - includeAllStatuses: bool - workspaceId: str - projectId: str - name: str - assigneeId: str - - -class TaskMoveWorkspace(TypedDict): - workspaceId: str - assigneeId: str | None - - -class RecurringTaskCreate(TypedDict): - frequency: str - deadlineType: str - duration: int | str - startingOn: str - idealTime: str | None - schedule: str - name: str - workspaceId: str - description: str | None - priority: str - assigneeId: str - - -class RecurringTaskListParams(TypedDict, total=False): - cursor: str - workspaceId: str - - -class TaskResource(Resource): +class TaskCreate(GenericTypedDict[Any]): + name: Required[str] + workspaceId: Required[str] + + dueDate: NotRequired[str] + duration: NotRequired[Union[Literal["NONE", "REMINDER"], int]] + status: NotRequired[str] + autoScheduled: NotRequired[dict[str, Any] | None] + projectId: NotRequired[str] + description: NotRequired[str] + priority: NotRequired[Literal["ASAP", "HIGH", "MEDIUM", "LOW"]] + labels: NotRequired[List[str]] + assigneeId: NotRequired[str] + + +class TaskUpdate(GenericTypedDict[Any], total=False): + name: NotRequired[str] + dueDate: NotRequired[str] + assigneeId: NotRequired[str | None] + duration: NotRequired[Union[Literal["NONE", "REMINDER"], int]] + status: NotRequired[str] + autoScheduled: NotRequired[dict[str, Any] | None] + projectId: NotRequired[str] + description: NotRequired[str] + priority: NotRequired[Literal["ASAP", "HIGH", "MEDIUM", "LOW"]] + labels: NotRequired[List[str]] + + +class TaskListParams(GenericTypedDict[Any], total=False): + cursor: NotRequired[str] + label: NotRequired[str] + status: NotRequired[List[str]] + includeAllStatuses: NotRequired[bool] + workspaceId: NotRequired[str] + projectId: NotRequired[str] + name: NotRequired[str] + assigneeId: NotRequired[str] + + +class TaskMoveWorkspace(GenericTypedDict[Any]): + workspaceId: Required[str] + assigneeId: NotRequired[str] + + +class RecurringTaskCreate(GenericTypedDict[Any]): + frequency: Required[str] + name: Required[str] + workspaceId: Required[str] + priority: Required[Literal["HIGH", "MEDIUM"]] + assigneeId: Required[str] + + deadlineType: NotRequired[Literal["HARD", "SOFT"]] + duration: NotRequired[Union[Literal["REMINDER"], int]] + startingOn: NotRequired[str] + idealTime: NotRequired[str] + schedule: NotRequired[str] + description: NotRequired[str] + + +class RecurringTaskCreateOptional(GenericTypedDict[Any], total=False): + # This TypedDict mirrors RecurringTaskCreate but every key is + # explicitly marked as NotRequired so that `PATCH` calls only need + # to include fields that should be modified. + + frequency: NotRequired[str] + deadlineType: NotRequired[Literal["HARD", "SOFT"]] + duration: NotRequired[Union[Literal["REMINDER"], int]] + startingOn: NotRequired[str] + idealTime: NotRequired[str] + schedule: NotRequired[str] + name: NotRequired[str] + workspaceId: NotRequired[str] + description: NotRequired[str] + priority: NotRequired[Literal["HIGH", "MEDIUM"]] + assigneeId: NotRequired[str] + + +class RecurringTaskListParams(GenericTypedDict[Any]): + workspaceId: Required[str] + cursor: NotRequired[str] + + +class TaskResource( + Resource[TaskCreate, TaskUpdate, TaskListParams, Task, ListTasks] +): base_path = "/tasks" - def create(self, data: TaskCreate) -> Task: - response = super().create(data) - return Task.model_validate(response.json()) - - def update(self, object_id: str, data: TaskUpdate) -> Task: - response = super().update(object_id, data) - return Task.model_validate(response.json()) + def _parse_model(self, data: Any) -> Task: + return Task.model_validate(data) - def list(self, params: TaskListParams | None = None) -> ListTasks: - response = super().list(params) - return ListTasks.model_validate(response.json()) + def _parse_list_model(self, data: Any) -> ListTasks: + return ListTasks.model_validate(data) - def retrieve(self, object_id: str) -> Task: - response = super().retrieve(object_id) + def patch(self, task_id: str, data: TaskUpdate) -> Task: + response = self._client.call_api( + HttpMethod.PATCH, + f"{self.base_path}/{task_id}", + data=data, + ) return Task.model_validate(response.json()) def unassign_task(self, task_id: str) -> None: self._client.call_api( HttpMethod.DELETE, - path=f"{self.base_path}/{task_id}/assignee", + f"{self.base_path}/{task_id}/assignee", ) def move_workspace(self, task_id: str, data: TaskMoveWorkspace) -> Task: response = self._client.call_api( HttpMethod.PATCH, - path=f"{self.base_path}/{task_id}/move", + f"{self.base_path}/{task_id}/move", data=data, ) return Task.model_validate(response.json()) -class RecurringTaskResource(Resource): +class RecurringTaskResource( + Resource[ + RecurringTaskCreate, + RecurringTaskCreateOptional, + RecurringTaskListParams, + RecurringTask, + ListRecurringTasks, + ] +): base_path = "/recurring-tasks" - def create(self, data: RecurringTaskCreate) -> RecurringTask: - response = super().create(data) - return RecurringTask.model_validate(response.json()) - - def list( - self, params: RecurringTaskListParams | None = None - ) -> ListRecurringTasks: - response = super().list(params) - return ListRecurringTasks.model_validate(response.json()) + def _parse_model(self, data: Any) -> RecurringTask: + return RecurringTask.model_validate(data) - def delete(self, object_id: str) -> None: - super().delete(object_id) + def _parse_list_model(self, data: Any) -> ListRecurringTasks: + return ListRecurringTasks.model_validate(data) diff --git a/src/motion/resources/user.py b/src/motion/resources/user.py index 04aff7c..58afd81 100644 --- a/src/motion/resources/user.py +++ b/src/motion/resources/user.py @@ -1,26 +1,36 @@ -from typing import TypedDict +from typing import Any -from ..client import HttpMethod +from ..client import GenericTypedDict, HttpMethod from ..models import ListUsers, User from .base import Resource -class UserListParams(TypedDict, total=False): +class UserListParams(GenericTypedDict[Any], total=False): cursor: str workspaceId: str teamId: str -class UserResource(Resource): +class UserResource( + Resource[ + GenericTypedDict[Any], + GenericTypedDict[Any], + UserListParams, + User, + ListUsers, + ] +): base_path = "/users" + def _parse_model(self, data: Any) -> User: + return User.model_validate(data) + + def _parse_list_model(self, data: Any) -> ListUsers: + return ListUsers.model_validate(data) + def get_self(self) -> User: response = self._client.call_api( HttpMethod.GET, - path=f"{self.base_path}/me", + f"{self.base_path}/me", ) return User.model_validate(response.json()) - - def list(self, params: UserListParams | None = None) -> ListUsers: - response = super().list(params) - return ListUsers.model_validate(response.json()) diff --git a/src/motion/resources/workspace.py b/src/motion/resources/workspace.py index b9225ae..b0adf5e 100644 --- a/src/motion/resources/workspace.py +++ b/src/motion/resources/workspace.py @@ -1,28 +1,28 @@ -from typing import TypedDict, List +from typing import Any, List -from ..client import HttpMethod -from ..models import ListWorkspaces, Status +from ..client import GenericTypedDict +from ..models import ListWorkspaces, Workspace from .base import Resource -class WorkspaceListParams(TypedDict, total=False): +class WorkspaceListParams(GenericTypedDict[Any], total=False): cursor: str ids: List[str] -class WorkspaceResource(Resource): +class WorkspaceResource( + Resource[ + GenericTypedDict[Any], + GenericTypedDict[Any], + WorkspaceListParams, + Workspace, + ListWorkspaces, + ] +): base_path = "/workspaces" - def list( - self, params: WorkspaceListParams | None = None - ) -> ListWorkspaces: - response = super().list(params) - return ListWorkspaces.model_validate(response.json()) + def _parse_model(self, data: Any) -> Workspace: + return Workspace.model_validate(data) - def list_statuses(self, workspace_id: str) -> List[Status]: - response = self._client.call_api( - HttpMethod.GET, - path="/statuses", - params={"workspaceId": workspace_id}, - ) - return [Status.model_validate(item) for item in response.json()] + def _parse_list_model(self, data: Any) -> ListWorkspaces: + return ListWorkspaces.model_validate(data) diff --git a/tests/conftest.py b/tests/conftest.py index bc3331a..4230ec0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ -import pytest from unittest.mock import Mock +import pytest + + @pytest.fixture -def mock_client(): - return Mock() \ No newline at end of file +def mock_client() -> Mock: + """Fixture providing a mocked HttpClient for testing""" + return Mock() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..17629e9 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,112 @@ +"""Test the HTTP client""" + +from typing import Any +from unittest.mock import Mock, patch + +import pytest +import requests + +from motion.client import GenericTypedDict, HttpClient, HttpMethod + + +def test_httpclient_initialization() -> None: + """Test HttpClient initialization""" + api_key = "test-api-key" + client = HttpClient(api_key) + + # Just verify it initializes without error + assert client is not None + + +@patch("motion.client.requests.request") +def test_call_api_success(mock_request: Mock) -> None: + """Test successful API call""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = {"id": "123", "name": "Test"} + mock_response.raise_for_status.return_value = None + mock_request.return_value = mock_response + + # Make API call + client = HttpClient("test-api-key") + response = client.call_api( + HttpMethod.GET, + "/tasks", + params={"workspaceId": "ws123"} + ) + + # Verify request was made correctly + mock_request.assert_called_once_with( + method="GET", + url="https://api.usemotion.com/v1/tasks", + json=None, + params={"workspaceId": "ws123"}, + headers={"X-API-Key": "test-api-key"} + ) + + # Verify response + assert response == mock_response + mock_response.raise_for_status.assert_called_once() + + +@patch("motion.client.requests.request") +def test_call_api_with_data(mock_request: Mock) -> None: + """Test API call with data payload""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = {"id": "123"} + mock_response.raise_for_status.return_value = None + mock_request.return_value = mock_response + + # Make API call + client = HttpClient("test-api-key") + data: GenericTypedDict[Any] = {"name": "New Task", "workspaceId": "ws123"} # type: ignore + response = client.call_api( + HttpMethod.POST, + "/tasks", + data=data + ) + + # Verify request was made correctly + mock_request.assert_called_once_with( + method="POST", + url="https://api.usemotion.com/v1/tasks", + json=data, + params=None, + headers={"X-API-Key": "test-api-key"} + ) + + # Verify response + assert response == mock_response + + +@patch("motion.client.requests.request") +def test_call_api_strips_leading_slash(mock_request: Mock) -> None: + """Test that leading slash is stripped from path""" + # Setup mock response + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_request.return_value = mock_response + + # Make API call with leading slash + client = HttpClient("test-api-key") + client.call_api(HttpMethod.GET, "/tasks") + + # Verify URL doesn't have double slash + mock_request.assert_called_once() + call_args = mock_request.call_args + assert call_args[1]["url"] == "https://api.usemotion.com/v1/tasks" + + +@patch("motion.client.requests.request") +def test_call_api_error_propagates(mock_request: Mock) -> None: + """Test that HTTP errors are propagated""" + # Setup mock response that raises on status check + mock_response = Mock() + mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found") + mock_request.return_value = mock_response + + # Make API call and expect error + client = HttpClient("test-api-key") + with pytest.raises(requests.HTTPError): + client.call_api(HttpMethod.GET, "/tasks/invalid") \ No newline at end of file diff --git a/tests/test_comment_resource.py b/tests/test_comment_resource.py index ceeb10f..0c6ff9f 100644 --- a/tests/test_comment_resource.py +++ b/tests/test_comment_resource.py @@ -1,23 +1,29 @@ -import pytest from datetime import datetime -from motion.resources.comment import CommentResource + +import pytest + from motion.models import Comment, ListComments, User +from motion.resources.comment import CommentResource + @pytest.fixture def comment_resource(mock_client): return CommentResource(mock_client) + def test_create_comment(comment_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "id": "123", "taskId": "task123", "content": "Test comment", "creator": {"id": "user1", "name": "John Doe"}, - "createdAt": "2023-06-01T12:00:00Z" + "createdAt": "2023-06-01T12:00:00Z", } - - comment = comment_resource.create({"taskId": "task123", "content": "Test comment"}) - + + comment = comment_resource.create( + {"taskId": "task123", "content": "Test comment"} + ) + assert isinstance(comment, Comment) assert comment.id == "123" assert comment.taskId == "task123" @@ -25,6 +31,7 @@ def test_create_comment(comment_resource, mock_client): assert isinstance(comment.creator, User) assert isinstance(comment.createdAt, datetime) + def test_list_comments(comment_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "comments": [ @@ -33,15 +40,15 @@ def test_list_comments(comment_resource, mock_client): "taskId": "task123", "content": "Test comment", "creator": {"id": "user1", "name": "John Doe"}, - "createdAt": "2023-06-01T12:00:00Z" + "createdAt": "2023-06-01T12:00:00Z", } ], - "meta": {"pageSize": 10} + "meta": {"pageSize": 10}, } - + comments = comment_resource.list({"taskId": "task123"}) - + assert isinstance(comments, ListComments) assert len(comments.comments) == 1 assert isinstance(comments.comments[0], Comment) - assert comments.meta.pageSize == 10 \ No newline at end of file + assert comments.meta.pageSize == 10 diff --git a/tests/test_motion.py b/tests/test_motion.py new file mode 100644 index 0000000..40b5463 --- /dev/null +++ b/tests/test_motion.py @@ -0,0 +1,29 @@ +"""Test the main Motion client class""" + +from motion import Motion +from motion.resources import ( + CommentResource, + ProjectResource, + RecurringTaskResource, + ScheduleResource, + StatusResource, + TaskResource, + UserResource, + WorkspaceResource, +) + + +def test_motion_initialization() -> None: + """Test that Motion class initializes all resources correctly""" + api_key = "test-api-key" + motion = Motion(api_key) + + # Check that all resources are initialized with correct types + assert isinstance(motion.tasks, TaskResource) + assert isinstance(motion.users, UserResource) + assert isinstance(motion.projects, ProjectResource) + assert isinstance(motion.comments, CommentResource) + assert isinstance(motion.workspaces, WorkspaceResource) + assert isinstance(motion.schedules, ScheduleResource) + assert isinstance(motion.statuses, StatusResource) + assert isinstance(motion.recurring_tasks, RecurringTaskResource) \ No newline at end of file diff --git a/tests/test_project_resource.py b/tests/test_project_resource.py index 8c10fc3..eb4a724 100644 --- a/tests/test_project_resource.py +++ b/tests/test_project_resource.py @@ -1,61 +1,80 @@ import pytest + +from motion.models import ListProjects, Project, Status from motion.resources.project import ProjectResource -from motion.models import Project, ListProjects, Status + @pytest.fixture def project_resource(mock_client): return ProjectResource(mock_client) + def test_create_project(project_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "id": "proj123", "name": "Test Project", "description": "A test project", "workspaceId": "ws123", - "status": {"name": "In Progress", "isDefaultStatus": False, "isResolvedStatus": False} + "status": { + "name": "In Progress", + "isDefaultStatus": False, + "isResolvedStatus": False, + }, } - - project = project_resource.create({ - "name": "Test Project", - "workspaceId": "ws123", - "description": "A test project", - "priority": "MEDIUM" - }) - + + project = project_resource.create( + { + "name": "Test Project", + "workspaceId": "ws123", + "description": "A test project", + "priority": "MEDIUM", + } + ) + assert isinstance(project, Project) assert project.id == "proj123" assert project.name == "Test Project" assert project.workspaceId == "ws123" assert isinstance(project.status, Status) + def test_list_projects(project_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "projects": [ { "id": "proj123", "name": "Test Project", - "status": {"name": "In Progress", "isDefaultStatus": False, "isResolvedStatus": False} + "status": { + "name": "In Progress", + "isDefaultStatus": False, + "isResolvedStatus": False, + }, } ], - "meta": {"pageSize": 10} + "meta": {"pageSize": 10}, } - + projects = project_resource.list({"workspaceId": "ws123"}) - + assert isinstance(projects, ListProjects) assert len(projects.projects) == 1 assert isinstance(projects.projects[0], Project) assert projects.meta.pageSize == 10 + def test_retrieve_project(project_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "id": "proj123", "name": "Test Project", - "status": {"name": "In Progress", "isDefaultStatus": False, "isResolvedStatus": False} + "status": { + "name": "In Progress", + "isDefaultStatus": False, + "isResolvedStatus": False, + }, } - + project = project_resource.retrieve("proj123") - + assert isinstance(project, Project) assert project.id == "proj123" - assert project.name == "Test Project" \ No newline at end of file + assert project.name == "Test Project" diff --git a/tests/test_recurring_task_resource.py b/tests/test_recurring_task_resource.py new file mode 100644 index 0000000..48835a5 --- /dev/null +++ b/tests/test_recurring_task_resource.py @@ -0,0 +1,128 @@ +from unittest.mock import Mock + +import pytest + +from motion.client import HttpMethod +from motion.resources.task import RecurringTaskResource + + +@pytest.fixture +def recurring_task_resource(mock_client: Mock) -> RecurringTaskResource: + return RecurringTaskResource(mock_client) + + +def test_create_recurring_task( + recurring_task_resource: RecurringTaskResource, mock_client: Mock +) -> None: + mock_client.call_api.return_value.json.return_value = { + "id": "rt123", + "name": "Daily Standup", + "priority": "HIGH", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "assignee": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + } + + recurring_task = recurring_task_resource.create( + { + "frequency": "daily_every_week_day", + "deadlineType": "SOFT", + "duration": 30, + "startingOn": "2024-03-12T06:00:00.000Z", + "idealTime": "09:00", + "schedule": "Work Hours", + "name": "Daily Standup", + "workspaceId": "ws123", + "description": "Daily team standup meeting", + "priority": "HIGH", + "assigneeId": "user123", + } + ) + + assert recurring_task.id == "rt123" + assert recurring_task.name == "Daily Standup" + assert recurring_task.priority == "HIGH" + mock_client.call_api.assert_called_once() + + +def test_list_recurring_tasks( + recurring_task_resource: RecurringTaskResource, mock_client: Mock +) -> None: + mock_client.call_api.return_value.json.return_value = { + "tasks": [ + { + "id": "rt123", + "name": "Daily Standup", + "priority": "HIGH", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "assignee": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + }, + { + "id": "rt124", + "name": "Weekly Review", + "priority": "MEDIUM", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "assignee": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + }, + ], + "meta": {"pageSize": 100}, + } + + recurring_tasks = recurring_task_resource.list({"workspaceId": "ws123"}) + + assert len(recurring_tasks.tasks) == 2 + assert recurring_tasks.tasks[0].id == "rt123" + assert recurring_tasks.tasks[1].id == "rt124" + assert recurring_tasks.meta is not None + assert recurring_tasks.meta.pageSize == 100 + mock_client.call_api.assert_called_once_with( + HttpMethod.GET, "/recurring-tasks", params={"workspaceId": "ws123"} + ) + + +def test_delete_recurring_task( + recurring_task_resource: RecurringTaskResource, mock_client: Mock +) -> None: + recurring_task_resource.delete("rt123") + + mock_client.call_api.assert_called_once_with( + HttpMethod.DELETE, "/recurring-tasks/rt123" + ) diff --git a/tests/test_schedule_resource.py b/tests/test_schedule_resource.py index c3d6c5e..dc8a029 100644 --- a/tests/test_schedule_resource.py +++ b/tests/test_schedule_resource.py @@ -1,11 +1,14 @@ import pytest -from motion.resources.schedule import ScheduleResource + from motion.models import Schedule +from motion.resources.schedule import ScheduleResource + @pytest.fixture def schedule_resource(mock_client): return ScheduleResource(mock_client) + def test_list_schedules(schedule_resource, mock_client): mock_client.call_api.return_value.json.return_value = [ { @@ -19,19 +22,19 @@ def test_list_schedules(schedule_resource, mock_client): "thursday": [{"start": "09:00", "end": "17:00"}], "friday": [{"start": "09:00", "end": "17:00"}], "saturday": [], - "sunday": [] - } + "sunday": [], + }, } ] - + schedules = schedule_resource.list() - + assert isinstance(schedules, list) assert len(schedules) == 1 assert isinstance(schedules[0], Schedule) assert schedules[0].name == "Work Hours" - assert schedules[0].isDefaultTimezone == True + assert schedules[0].isDefaultTimezone assert schedules[0].timezone == "America/New_York" assert len(schedules[0].schedule.monday) == 1 assert schedules[0].schedule.monday[0].start == "09:00" - assert schedules[0].schedule.monday[0].end == "17:00" \ No newline at end of file + assert schedules[0].schedule.monday[0].end == "17:00" diff --git a/tests/test_schema_compliance.py b/tests/test_schema_compliance.py new file mode 100644 index 0000000..037638a --- /dev/null +++ b/tests/test_schema_compliance.py @@ -0,0 +1,152 @@ +""" +Test schema compliance for Motion API models +""" + +from typing import Any + +from motion.models import ( + AutoScheduledInfo, + CommentPost, + ProjectPost, + RecurringTask, + Schedule, + Task, + TaskPost, +) + + +def test_task_model_schema_compliance(): + """Test that Task model matches the schema""" + task_data: dict[str, Any] = { + "id": "task123", + "name": "Test Task", + "description": "A test task", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 30, + "priority": "HIGH", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + + task = Task.model_validate(task_data) + assert task.id == "task123" + assert task.priority == "HIGH" + assert task.deadlineType == "SOFT" + assert task.duration == 30 + + +def test_task_post_schema_compliance() -> None: + """Test that TaskPost model matches the schema""" + task_post = TaskPost( + name="New Task", + workspaceId="ws123", + priority="MEDIUM", + duration="NONE", + autoScheduled=AutoScheduledInfo( + deadlineType="HARD", schedule="Work Hours" + ), + ) + + data = task_post.model_dump(exclude_none=True) + assert data["name"] == "New Task" + assert data["priority"] == "MEDIUM" + assert data["duration"] == "NONE" + assert data["autoScheduled"]["deadlineType"] == "HARD" + + +def test_comment_post_schema_compliance(): + """Test that CommentPost model matches the schema""" + comment_post = CommentPost( + taskId="task123", content="This is a test comment" + ) + + data = comment_post.model_dump() + assert data["taskId"] == "task123" + assert data["content"] == "This is a test comment" + + +def test_project_post_schema_compliance(): + """Test that ProjectPost model matches the schema""" + project_post = ProjectPost( + name="New Project", + workspaceId="ws123", + priority="HIGH", + description="A test project", + ) + + data = project_post.model_dump(exclude_none=True) + assert data["name"] == "New Project" + assert data["priority"] == "HIGH" + assert data["workspaceId"] == "ws123" + + +def test_schedule_model_schema_compliance(): + """Test that Schedule model matches the schema""" + schedule_data = { + "name": "Work Hours", + "isDefaultTimezone": True, + "timezone": "America/New_York", + "schedule": { + "monday": [{"start": "09:00", "end": "17:00"}], + "tuesday": [{"start": "09:00", "end": "17:00"}], + "wednesday": [{"start": "09:00", "end": "17:00"}], + "thursday": [{"start": "09:00", "end": "17:00"}], + "friday": [{"start": "09:00", "end": "17:00"}], + "saturday": [], + "sunday": [], + }, + } + + schedule = Schedule.model_validate(schedule_data) + assert schedule.name == "Work Hours" + assert schedule.isDefaultTimezone is True + assert len(schedule.schedule.monday) == 1 + assert schedule.schedule.monday[0].start == "09:00" + assert len(schedule.schedule.saturday) == 0 + + +def test_recurring_task_schema_compliance(): + """Test that RecurringTask model matches the schema""" + recurring_task_data: dict[str, Any] = { + "id": "rt123", + "name": "Daily Standup", + "priority": "HIGH", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "assignee": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + } + + recurring_task = RecurringTask.model_validate(recurring_task_data) + assert recurring_task.id == "rt123" + assert recurring_task.priority == "HIGH" + assert recurring_task.name == "Daily Standup" diff --git a/tests/test_status_resource.py b/tests/test_status_resource.py new file mode 100644 index 0000000..abb120d --- /dev/null +++ b/tests/test_status_resource.py @@ -0,0 +1,31 @@ +import pytest + +from motion.models import Status +from motion.resources.status import StatusResource + + +@pytest.fixture +def status_resource(mock_client): + return StatusResource(mock_client) + + +def test_list_statuses(status_resource, mock_client): + # Mock the API response + mock_client.call_api.return_value.json.return_value = [ + {"name": "To Do", "isDefaultStatus": True, "isResolvedStatus": False}, + {"name": "Done", "isDefaultStatus": False, "isResolvedStatus": True}, + ] + + # Call the method + statuses = status_resource.list({"workspaceId": "workspace123"}) + + # Verify the response + assert len(statuses) == 2 + assert isinstance(statuses[0], Status) + assert statuses[0].name == "To Do" + assert statuses[0].isDefaultStatus is True + assert statuses[0].isResolvedStatus is False + assert isinstance(statuses[1], Status) + assert statuses[1].name == "Done" + assert statuses[1].isDefaultStatus is False + assert statuses[1].isResolvedStatus is True diff --git a/tests/test_task_resource.py b/tests/test_task_resource.py new file mode 100644 index 0000000..1c774c9 --- /dev/null +++ b/tests/test_task_resource.py @@ -0,0 +1,235 @@ +from unittest.mock import Mock + +import pytest + +from motion.client import HttpMethod +from motion.resources.task import TaskResource + + +@pytest.fixture +def task_resource(mock_client: Mock) -> TaskResource: + return TaskResource(mock_client) + + +def test_create_task(task_resource: TaskResource, mock_client: Mock) -> None: + mock_client.call_api.return_value.json.return_value = { + "id": "task123", + "name": "New Task", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 30, + "priority": "MEDIUM", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + + task = task_resource.create({"name": "New Task", "workspaceId": "ws123"}) + + assert task.id == "task123" + assert task.name == "New Task" + assert task.priority == "MEDIUM" + mock_client.call_api.assert_called_once_with( + HttpMethod.POST, + "/tasks", + data={"name": "New Task", "workspaceId": "ws123"}, + ) + + +def test_list_tasks(task_resource: TaskResource, mock_client: Mock) -> None: + mock_client.call_api.return_value.json.return_value = { + "tasks": [ + { + "id": "task123", + "name": "Task 1", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 30, + "priority": "HIGH", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + ], + "meta": {"pageSize": 100}, + } + + tasks = task_resource.list({"workspaceId": "ws123"}) + + assert len(tasks.tasks) == 1 + assert tasks.tasks[0].id == "task123" + assert tasks.meta is not None + assert tasks.meta.pageSize == 100 + + +def test_retrieve_task(task_resource: TaskResource, mock_client: Mock) -> None: + mock_client.call_api.return_value.json.return_value = { + "id": "task123", + "name": "Task 1", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 30, + "priority": "HIGH", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + + task = task_resource.retrieve("task123") + + assert task.id == "task123" + assert task.name == "Task 1" + mock_client.call_api.assert_called_once_with( + HttpMethod.GET, "/tasks/task123" + ) + + +def test_patch_task(task_resource: TaskResource, mock_client: Mock) -> None: + mock_client.call_api.return_value.json.return_value = { + "id": "task123", + "name": "Updated Task", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 60, + "priority": "HIGH", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws123", + "name": "Test Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "In Progress", + "isDefaultStatus": False, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + + task = task_resource.patch( + "task123", {"name": "Updated Task", "duration": 60} + ) + + assert task.id == "task123" + assert task.name == "Updated Task" + assert task.duration == 60 + mock_client.call_api.assert_called_once_with( + HttpMethod.PATCH, + "/tasks/task123", + data={"name": "Updated Task", "duration": 60}, + ) + + +def test_delete_task(task_resource: TaskResource, mock_client: Mock) -> None: + task_resource.delete("task123") + + mock_client.call_api.assert_called_once_with( + HttpMethod.DELETE, "/tasks/task123" + ) + + +def test_unassign_task(task_resource: TaskResource, mock_client: Mock) -> None: + task_resource.unassign_task("task123") + + mock_client.call_api.assert_called_once_with( + HttpMethod.DELETE, "/tasks/task123/assignee" + ) + + +def test_move_workspace( + task_resource: TaskResource, mock_client: Mock +) -> None: + mock_client.call_api.return_value.json.return_value = { + "id": "task123", + "name": "Moved Task", + "dueDate": "2024-03-12T10:52:55.724-06:00", + "deadlineType": "SOFT", + "completed": False, + "duration": 30, + "priority": "MEDIUM", + "schedulingIssue": False, + "createdTime": "2024-03-12T10:52:55.724-06:00", + "workspace": { + "id": "ws456", + "name": "New Workspace", + "type": "TEAM", + "statuses": [], + "labels": [], + }, + "creator": {"id": "user123", "name": "John Doe"}, + "status": { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + "labels": [], + "assignees": [], + "parentRecurringTaskId": None, + } + + task = task_resource.move_workspace( + "task123", {"workspaceId": "ws456", "assigneeId": "user456"} + ) + + assert task.id == "task123" + assert task.workspace.id == "ws456" + mock_client.call_api.assert_called_once_with( + HttpMethod.PATCH, + "/tasks/task123/move", + data={"workspaceId": "ws456", "assigneeId": "user456"}, + ) diff --git a/tests/test_user_resource.py b/tests/test_user_resource.py index 5dfc60a..2c77274 100644 --- a/tests/test_user_resource.py +++ b/tests/test_user_resource.py @@ -1,47 +1,43 @@ import pytest + +from motion.models import ListUsers, User from motion.resources.user import UserResource -from motion.models import User, ListUsers + @pytest.fixture def user_resource(mock_client): return UserResource(mock_client) + def test_get_self(user_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "id": "user1", "name": "John Doe", - "email": "john@example.com" + "email": "john@example.com", } - + user = user_resource.get_self() - + assert isinstance(user, User) assert user.id == "user1" assert user.name == "John Doe" assert user.email == "john@example.com" + def test_list_users(user_resource, mock_client): mock_client.call_api.return_value.json.return_value = { "users": [ - { - "id": "user1", - "name": "John Doe", - "email": "john@example.com" - }, - { - "id": "user2", - "name": "Jane Doe", - "email": "jane@example.com" - } + {"id": "user1", "name": "John Doe", "email": "john@example.com"}, + {"id": "user2", "name": "Jane Doe", "email": "jane@example.com"}, ], - "meta": {"pageSize": 10} + "meta": {"pageSize": 10}, } - + users = user_resource.list({"workspaceId": "ws123"}) - + assert isinstance(users, ListUsers) assert len(users.users) == 2 assert isinstance(users.users[0], User) assert users.users[0].id == "user1" assert users.users[1].id == "user2" - assert users.meta.pageSize == 10 \ No newline at end of file + assert users.meta.pageSize == 10 diff --git a/tests/test_workspace_resource.py b/tests/test_workspace_resource.py new file mode 100644 index 0000000..d5cda27 --- /dev/null +++ b/tests/test_workspace_resource.py @@ -0,0 +1,104 @@ +from unittest.mock import Mock + +import pytest + +from motion.client import HttpMethod +from motion.resources.workspace import WorkspaceResource + + +@pytest.fixture +def workspace_resource(mock_client: Mock) -> WorkspaceResource: + return WorkspaceResource(mock_client) + + +def test_list_workspaces( + workspace_resource: WorkspaceResource, mock_client: Mock +) -> None: + mock_client.call_api.return_value.json.return_value = { + "workspaces": [ + { + "id": "ws123", + "name": "Personal Workspace", + "type": "PERSONAL", + "statuses": [ + { + "name": "To Do", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + { + "name": "Done", + "isDefaultStatus": False, + "isResolvedStatus": True, + }, + ], + "labels": [{"name": "Bug"}, {"name": "Feature"}], + }, + { + "id": "ws456", + "name": "Team Workspace", + "type": "TEAM", + "teamId": "team123", + "statuses": [ + { + "name": "Backlog", + "isDefaultStatus": True, + "isResolvedStatus": False, + }, + { + "name": "In Progress", + "isDefaultStatus": False, + "isResolvedStatus": False, + }, + { + "name": "Complete", + "isDefaultStatus": False, + "isResolvedStatus": True, + }, + ], + "labels": [{"name": "Priority"}, {"name": "Blocked"}], + }, + ], + "meta": {"pageSize": 100}, + } + + workspaces = workspace_resource.list() + + assert len(workspaces.workspaces) == 2 + assert workspaces.workspaces[0].id == "ws123" + assert workspaces.workspaces[0].name == "Personal Workspace" + assert workspaces.workspaces[1].id == "ws456" + assert workspaces.workspaces[1].teamId == "team123" + assert len(workspaces.workspaces[0].statuses) == 2 + assert len(workspaces.workspaces[0].labels) == 2 + assert workspaces.meta is not None + assert workspaces.meta.pageSize == 100 + mock_client.call_api.assert_called_once_with( + HttpMethod.GET, "/workspaces", params=None + ) + + +def test_list_workspaces_with_ids( + workspace_resource: WorkspaceResource, mock_client: Mock +) -> None: + mock_client.call_api.return_value.json.return_value = { + "workspaces": [ + { + "id": "ws123", + "name": "Personal Workspace", + "type": "PERSONAL", + "statuses": [], + "labels": [], + } + ], + "meta": {"pageSize": 100, "nextCursor": "cursor123"}, + } + + workspaces = workspace_resource.list({"ids": ["ws123", "ws456"]}) + + assert len(workspaces.workspaces) == 1 + assert workspaces.meta is not None + assert workspaces.meta.nextCursor == "cursor123" + mock_client.call_api.assert_called_once_with( + HttpMethod.GET, "/workspaces", params={"ids": ["ws123", "ws456"]} + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e7cfc04 --- /dev/null +++ b/uv.lock @@ -0,0 +1,442 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, + { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, + { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, + { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, + { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, + { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, + { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, + { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, + { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, + { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "motion" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.11.5" }, + { name = "requests", specifier = ">=2.32.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.401" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "ruff", specifier = ">=0.11.12" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.401" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/9a/7ab2b333b921b2d6bfcffe05a0e0a0bbeff884bd6fb5ed50cd68e2898e53/pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1", size = 3894193, upload-time = "2025-05-21T10:44:52.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e6/1f908fce68b0401d41580e0f9acc4c3d1b248adcff00dfaad75cd21a1370/pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06", size = 5629193, upload-time = "2025-05-21T10:44:50.129Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, + { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, + { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +]