pax_global_header00006660000000000000000000000064150067164150014517gustar00rootroot0000000000000052 comment=98d42824c265a92f052873403f3b640a1f2e5e7a todoist-api-python-3.1.0/000077500000000000000000000000001500671641500152735ustar00rootroot00000000000000todoist-api-python-3.1.0/.github/000077500000000000000000000000001500671641500166335ustar00rootroot00000000000000todoist-api-python-3.1.0/.github/CODEOWNERS000066400000000000000000000000211500671641500202170ustar00rootroot00000000000000* @Doist/Backend todoist-api-python-3.1.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001500671641500210165ustar00rootroot00000000000000todoist-api-python-3.1.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013451500671641500235130ustar00rootroot00000000000000--- name: Bug report about: Standard template for reporting bugs title: '' labels: 'bug' assignees: '' --- ## Bug description ## Expected behaviour ## Is reproducible Yes/No ## To reproduce ## Steps taken to try to reproduce ## Screenshots ## Version information: - Package version: - Python version: ## Additional information todoist-api-python-3.1.0/.github/ISSUE_TEMPLATE/enhancement.md000066400000000000000000000010761500671641500236310ustar00rootroot00000000000000--- name: Enhancement request about: Standard template for enhancement requests title: '' labels: 'enhancement' assignees: '' --- ## Enhancement description ## The problem it solves ## Alternatives ## Use case / screenshots ## Additional information todoist-api-python-3.1.0/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000003071500671641500232070ustar00rootroot00000000000000--- name: Question about: Standard template for reporting questions title: '' labels: 'question' assignees: '' --- ## Question description todoist-api-python-3.1.0/.github/renovate.json000066400000000000000000000002231500671641500213460ustar00rootroot00000000000000{ "extends": [ "github>doist/renovate-config:integrations-base", "github>doist/renovate-config:integrations-automerge" ] } todoist-api-python-3.1.0/.github/workflows/000077500000000000000000000000001500671641500206705ustar00rootroot00000000000000todoist-api-python-3.1.0/.github/workflows/docs.yml000066400000000000000000000011361500671641500223440ustar00rootroot00000000000000name: Publish docs on: push: branches: - main permissions: contents: write jobs: build-and-publish-docs: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version-file: .python-version - name: Set up uv uses: astral-sh/setup-uv@v5 with: version: 0.6.11 - name: Install project run: uv sync --group docs - name: Deploy to GitHub Pages run: uv run mkdocs gh-deploy --no-history todoist-api-python-3.1.0/.github/workflows/publish.yml000066400000000000000000000012261500671641500230620ustar00rootroot00000000000000name: Release package on: push: tags: - "v*" jobs: build-test: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set up Python uses: actions/setup-python@v5 with: python-version-file: .python-version - name: Set up uv uses: astral-sh/setup-uv@v5 with: version: 0.6.11 - name: Install project run: uv sync - name: Build and publish to PyPI env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} run: | uv build uv publish todoist-api-python-3.1.0/.github/workflows/test.yml000066400000000000000000000010251500671641500223700ustar00rootroot00000000000000name: Run tests on: [pull_request, workflow_dispatch] jobs: test: strategy: matrix: env: ["py39", "py310", "py311", "py312", "py313"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set up uv uses: astral-sh/setup-uv@v5 with: version: 0.6.11 - name: Install project run: uv sync --group dev - name: Test with pytest run: uv run tox -e ${{ matrix.env }} todoist-api-python-3.1.0/.gitignore000066400000000000000000000001141500671641500172570ustar00rootroot00000000000000.idea .mypy_cache .pytest_cache .ruff_cache .venv .vscode __pycache__/ dist todoist-api-python-3.1.0/.pre-commit-config.yaml000066400000000000000000000017211500671641500215550ustar00rootroot00000000000000default_language_version: python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: mixed-line-ending - id: check-merge-conflict - id: check-case-conflict - id: debug-statements - id: check-ast - id: check-json - id: check-symlinks - id: destroyed-symlinks - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.2 hooks: # Run the linter - id: ruff args: ["--fix"] # Run the formatter - id: ruff-format - repo: https://github.com/astral-sh/uv-pre-commit rev: 0.6.11 hooks: - id: uv-lock - repo: local hooks: - id: mypy name: mypy entry: uv run mypy language: system "types_or": [python, pyi] args: ["--scripts-are-modules"] require_serial: true todoist-api-python-3.1.0/.python-version000066400000000000000000000000051500671641500202730ustar00rootroot000000000000003.13 todoist-api-python-3.1.0/CHANGELOG.md000066400000000000000000000066741500671641500171210ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [3.1.0] - 2025-05-07 ### Added - Support for moving tasks, courtesy of @radiant-tangent - Support for `backups:read` scope - Re-add support for `X-Request-ID` - Configurable via `request_id_fn` API constructor argument - Defaults to random UUID v4 - Automatic testing across all supported Python versions ### Fixed - Compatibility with Python 3.9 and Python 3.10 ## [3.0.1] - 2025-04-15 ### Fixed - Wheel and source distributions didn't include the package itself - Project requiring Python 3.13 to be installed ## [3.0.0] - 2025-04-11 ### Added - Support for deadlines - Support for archiving and unarchiving projects - Support for fetching completed tasks by due date range and by completion date range - Support for `note`, `reminder`, and `auto_reminder` in `add_task_quick` - Documentation for all SDK functions, arguments, and return objects - Types, type hints for all SDK functions, arguments, and return objects - Function to obtain project URLs ### Changed - Use `dataclass-wizard` for object mapping - Modernized SDK to use the Todoist API v1 - Remove deprecated `Task.sync_id`, `Task.comment_count`, and `Project.comment_count` - Replace `Task.is_completed` with `Task.completed_at` - Add support for `calendar` in `Project.view_style` - Rename `quick_add_task` to `add_task_quick` - Add `filter_tasks`, extracting that workflow from `get_tasks` - Paginate results via an `Iterator` in `get_tasks`, `filter_task`, `get_projects`, `get_collaborators`, `get_sections`, `get_comments`, `get_labels`, `get_shared_labels` - Receive `date` and `datetime` arguments as objects, not strings - Remove support for `X-Request-Id` header, unused on the API level - "Hide" internal modules and functions - Task URLs are now obtained on demand, improving performance when not needed ### Fixed - API requests configure appropriate timeouts to avoid connections hanging ## [2.1.7] - 2024-08-13 ### Fixes - Regression with some `Project` object attributes ## [2.1.6] - 2024-08-07 ### Fixes - `TodoistAPIAsync` accepts a `session` parameter - State becomes optional in `AuthResult.from_dict()` - Duration handling in `to_dict()` and tests - Default value to `section_id` - Properly close requests `Session` object ## [2.1.5] - 2024-05-22 ### Fixes - Key error on `can_assign_tasks` in `Project` model ## [2.1.4] - 2024-05-07 ### Added - Support `project.can_assign_tasks` - Add `duration` to `Task` object - Pagination example ## [2.1.3] - 2023-08-15 ### Added - Support for getting completed items through the items archive ## [2.1.2] - 2023-08-14 ### Fixes - Restore Python 3.9 compatibility ## [2.1.1] - 2023-08-09 ### Fixes - Building environment updates ## [2.1.0] - 2023-08-02 ### Changed - Use built-in data classes instead of `attrs` ## [2.0.2] - 2022-11-02 ### Fixes - Task property `date_added` should be `added_at` ## [2.0.1] - 2022-10-06 ### Fixes - Fixed a crash in `get_comments` if attachment is null. ## [2.0.0] - 2022-09-08 Migrate to [REST API v2](https://developer.todoist.com/rest/v2/?python). ## [1.1.1] - 2022-02-15 ### Fixes - Add missing `attrs` package dependency ### Security - Dependabot updates ## [1.1.0] - 2021-11-23 ### Added - Public release todoist-api-python-3.1.0/LICENSE000066400000000000000000000020601500671641500162760ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021 Doist Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. todoist-api-python-3.1.0/README.md000066400000000000000000000036431500671641500165600ustar00rootroot00000000000000# Todoist API Python Client This is the official Python SDK for the Todoist API. ## Installation ```bash pip install todoist-api-python ``` Or add the project as a dependency in `pyproject.toml`: ```toml dependencies = [ "todoist-api-python>=3.1.0,<4", ] ``` ### Supported Python Versions Python version 3.9 and above. ## Usage Here's an example of initializing the API client, fetching a task, and paginating through its comments: ```python from todoist_api_python.api import TodoistAPI api = TodoistAPI("YOUR_API_TOKEN") task = api.get_task("6X4Vw2Hfmg73Q2XR") print(f"Task: {task.content}") comments_iter = api.get_comments(task_id=task.id) for comments in comments_iter: for comment in comments: print(f"Comment: {comment.content}") ``` ## Documentation For more detailed reference documentation, have a look at the [SDK documentation](https://doist.github.io/todoist-api-python/) and the [API documentation](https://developer.todoist.com). ## Development To install Python dependencies: ```sh $ uv sync ``` To install pre-commit: ```sh $ uv run pre-commit install ``` You can try your changes via REPL by running: ```sh $ uv run python ``` You can then import the library as described in [Usage](#usage) without having to create a file. If you decide to use `TodoistAPIAsync`, please keep in mind that you have to `import asyncio` and run `asyncio.run(yourmethod())` to make your async methods run as expected. ### Releases This API client is public, and available in a PyPI repository. A new update is automatically released by GitHub Actions, by creating a release with a tag in the format `vX.Y.Z` (`v..`). Users of the API client can then update to the new version in their `pyproject.toml` file. ## Feedback Any feedback, bugs, questions, comments, etc., can be reported as *Issues* in this repository. ### Contributions We would love contributions! *Pull requests* are welcome. todoist-api-python-3.1.0/SECURITY.md000066400000000000000000000010231500671641500170600ustar00rootroot00000000000000# Security Policy ## Supported Versions Only the latest released version is being supported with security updates. ## Reporting a Vulnerability If you discover any security vulnerability, please open an Issue at the project's GitHub repository, and we'll do our best to fix it right away. At Doist, our bug bounty program is a critical component of our security efforts. Your efforts may be eligible for a monetary reward. For more see our [Doist bug bounty policy](https://todoist.com/help/articles/doist-bug-bounty-policy). todoist-api-python-3.1.0/docs/000077500000000000000000000000001500671641500162235ustar00rootroot00000000000000todoist-api-python-3.1.0/docs/api.md000066400000000000000000000001401500671641500173110ustar00rootroot00000000000000# API Client ::: todoist_api_python.api.TodoistAPI ::: todoist_api_python.api.ResultsPaginator todoist-api-python-3.1.0/docs/api_async.md000066400000000000000000000001071500671641500205110ustar00rootroot00000000000000# API Client (async) ::: todoist_api_python.api_async.TodoistAPIAsync todoist-api-python-3.1.0/docs/assets/000077500000000000000000000000001500671641500175255ustar00rootroot00000000000000todoist-api-python-3.1.0/docs/assets/favicon.ico000066400000000000000000000145661500671641500216620ustar00rootroot00000000000000 (&  (N(  *,@1C1C1C1C1C1C1C1C1C1C1C1C,@*.?2E1C1C1C1C1C1C1C1C1C1C1C1C2E,@1C/A->/@->1C1C1C1C1C1C1C1C1C1C1C);@Qer->-?1C1C1C1C1C1C1C1C1CN\+;0A1C1C1C1C1C1C1CL[WfUc=M,=1C1C1C1C1C1CDTP^guw0A1C1C1C1C1CozFV1C1C1C1C1C1CP_r}KYy|1B1B1C1C1C1C1C]jFV1C1C1C1C1C1CIY4EzguUcZh1B1C1C1C1C1CVe-@/@1B+;>NTb1B1C1C1C1C1C-?1C1C1C1C0A+=N]1B1C1C1C1C1C1C1C1C1C1C1C1C-@->0B1C1C1C1C1C1C.?2E1C1C1C1C1C1C1C1C1C1C1C1C2E.?*,@1C1C1C1C1C1C1C1C1C1C1C1C,@*( @ /B1B2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C1B/B/B2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C/B1B2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C1B2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C1C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2Ccp3D2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2CJYcp2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C9IKZ2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C6G2Co{:K2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2CDS2C2C2C2C6F|2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C\j2C2C8GM\2C2CBR_l2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C8G2C2CVdGV2C2C2C2C2C2C2C2C2C2C2C2C2C2Cdq2C2C2Cs2C2C2C2C2C2C2C2C2C2C2C2Chu2C2C7H2C2C2C2C2C2C2C2C2C2C2C2C8G2C2C2CIX2C2CAQR`2C2C2C2C2C2C2C2C2C2C2C2C2CP^2C2C:JDS2C2C6G4E2C2C2C2C2C2C2C2C2C2C2C2C2C2C2Cu2C2C6GDS2C2CJY2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2Co{6G2C2Cmxan2C2C2C2C2C2C2C2C2C2C2C2CQ_p|2C2C2C2C2C2C2C2C2C2C2C2C2C2C2Ccp2C2CJYUc2C2C9IVd2C2C2C2C2C2C2C2C2C2C2C2C3D2C2C2C2C2CanAQ2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C>N2C2C2C2C2C2C2C2C2C5E2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C todoist-api-python-3.1.0/docs/authentication.md000066400000000000000000000017711500671641500215720ustar00rootroot00000000000000# Authentication This module provides functions to help authenticate with Todoist using the OAuth protocol. ## Quick start ```python import uuid from todoist_api_python.authentication import get_access_token, get_authentication_url # 1. Generate a random state state = uuid.uuid4() # 2. Get authorization url url = get_authentication_url( client_id="YOUR_CLIENT_ID", scopes=["data:read", "task:add"], state=uuid.uuid4() ) # 3.Redirect user to url # 4. Handle OAuth callback and get code code = "CODE_YOU_OBTAINED" # 5. Exchange code for access token auth_result = get_access_token( client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", code=code, ) # 6. Ensure state is consistent, and done! assert(auth_result.state == state) access_token = auth_result.access_token ``` For detailed implementation steps and security considerations, refer to the [Todoist OAuth documentation](https://todoist.com/api/v1/docs#tag/Authorization/OAuth). ::: todoist_api_python.authentication todoist-api-python-3.1.0/docs/changelog.md000066400000000000000000000000261500671641500204720ustar00rootroot00000000000000--8<-- "CHANGELOG.md" todoist-api-python-3.1.0/docs/index.md000066400000000000000000000016261500671641500176610ustar00rootroot00000000000000# Overview This is the official Python SDK for the Todoist API. ## Installation ```bash pip install todoist-api-python ``` Or add the project as a dependency in `pyproject.toml`: ```toml dependencies = [ "todoist-api-python>=3.1.0,<4", ] ``` ## Usage Here's an example of initializing the API client, fetching a task, and paginating through its comments: ```python from todoist_api_python.api import TodoistAPI api = TodoistAPI("YOUR_API_TOKEN") task = api.get_task("6X4Vw2Hfmg73Q2XR") print(f"Task: {task.content}") comments_iter = api.get_comments(task_id=task.id) for comments in comments_iter: for comment in comments: print(f"Comment: {comment.content}") ``` ## Quick start - [Authentication](authentication.md) - [API client](api.md) - [Models](models.md) ## API reference For detailed reference documentation, have a look at the [API documentation](https://developer.todoist.com/). todoist-api-python-3.1.0/docs/models.md000066400000000000000000000002211500671641500200230ustar00rootroot00000000000000# Models ::: todoist_api_python.models options: show_if_no_docstring: true show_labels: false members_order: alphabetical todoist-api-python-3.1.0/mkdocs.yml000066400000000000000000000027001500671641500172750ustar00rootroot00000000000000site_name: "Todoist Python SDK" repo_url: https://github.com/Doist/todoist-api-python/ nav: - index.md - authentication.md - api.md - api_async.md - models.md - changelog.md theme: name: material logo: assets/logo.svg favicon: assets/favicon.ico palette: - media: "(prefers-color-scheme)" primary: red toggle: icon: material/brightness-4 name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: black toggle: icon: material/brightness-7 name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: red toggle: icon: material/brightness-auto name: Follow system preference features: - navigation.footer extra: social: - icon: fontawesome/brands/x-twitter link: https://x.com/doistdevs - icon: fontawesome/brands/github link: https://github.com/doist markdown_extensions: - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences plugins: - search - mkdocstrings: handlers: python: options: docstring_style: sphinx backlinks: true filters: ["!^_"] show_symbol_type_toc: True signature_crossrefs: true unwrap_annotated: true todoist-api-python-3.1.0/pyproject.toml000066400000000000000000000076301500671641500202150ustar00rootroot00000000000000[project] name = "todoist_api_python" version = "3.1.0" description = "Official Python SDK for the Todoist API." authors = [{ name = "Doist Developers", email = "dev@doist.com" }] requires-python = "~=3.9" readme = "README.md" license = "MIT" keywords = ["todoist", "rest", "sync", "api", "python"] classifiers = [ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "requests>=2.32.3,<3", "dataclass-wizard>=0.35.0,<1.0", "annotated-types", ] [project.urls] Homepage = "https://github.com/Doist/todoist-api-python" Repository = "https://github.com/Doist/todoist-api-python" Documentation = "https://developer.todoist.com/rest/" [dependency-groups] dev = [ "pre-commit>=4.0.0,<5", "pytest>=8.0.0,<9", "pytest-asyncio>=0.26.0,<0.27", "tox>=4.15.1,<5", "tox-uv>=1.25.0,<2", "mypy~=1.11", "ruff>=0.11.0,<0.12", "responses>=0.25.3,<0.26", "types-requests~=2.32", ] docs = [ "mkdocs>=1.6.1,<2.0.0", "mkdocstrings[python]>=0.29.1,<1.0.0", "mkdocs-material>=9.6.11,<10.0.0", ] [tool.hatch.build.targets.wheel] packages = ["todoist_api_python"] [tool.hatch.build.targets.sdist] packages = ["todoist_api_python"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.mypy] follow_imports = "silent" mypy_path = "." scripts_are_modules = true namespace_packages = true no_implicit_optional = true no_implicit_reexport = true show_error_codes = true check_untyped_defs = true enable_error_code = [ "redundant-self", "redundant-expr", "ignore-without-code", "truthy-iterable", "truthy-bool", ] extra_checks = true strict_equality = true strict_optional = true # default value, but required for Pylance to be strict, see https://twist.com/a/1585/ch/274843/t/3453725/c/76267088 warn_redundant_casts = true warn_unused_ignores = true disallow_any_generics = true untyped_calls_exclude = [] [tool.pydantic-mypy] init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.ruff] target-version = "py39" # used by some linters like UP, FA, PERF [tool.ruff.lint] select = [ "A", # flake8-builtins "ASYNC", # flake8-async "B", # flake8-bugbear "C4", # flake8-comprehensions "D", # pydocstyle, "DTZ", # flake8-datetimez, "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "PL", # pylint "RUF", # ruff "S", # flake8-bandit "T20", # flake8-print "SIM", # flake8-simplify "UP", # pyupgrade "TC", # flake8-type-checking "TRY", # tryceratops "BLE", # flake8-blind-except "FA", # flake8-future-annotations "FIX", # flake8-fixme "ICN", # flake8-import-conventions "LOG", # flake8-logging "G", # flake8-logging-format "RET", # flake8-logging-return "ISC", # flake8-implicit-str-concat "INP", # flake8-no-pep420 "PIE", # flake8-pie "PT", # flake8-pytest-style "TID", # flake8-tidy-imports "PTH", # flake8-user-pathlib "PERF", # perflint "FURB", # refurb "N", # pep8-naming "PGH", # pygrep-hooks "PYI", # flake8-pyi "ANN", # flake8-annotations ] ignore = [ "D203", # incorrect-blank-line-before-class "D212", # multi-line-summary-first-line "PLR0913", # too-many-arguments "TRY00", # raise-vanilla-args # All listed below are not intentional and should be fixed "D100", # undocumented-public-module "D101", # undocumented-public-class "D102", # undocumented-public-method "D103", # undocumented-public-function "D104", # undocumented-public-package ] [tool.ruff.lint.extend-per-file-ignores] "tests/**/*.py" = [ "S101", # assert "S105", # hardcoded-password-string "PLR2004", # magic-value-comparison ] [tool.ruff.lint.pydocstyle] convention = "pep257" [tool.ruff.format] docstring-code-format = true [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" todoist-api-python-3.1.0/tests/000077500000000000000000000000001500671641500164355ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/__init__.py000066400000000000000000000000001500671641500205340ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/conftest.py000066400000000000000000000121461500671641500206400ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import ( DEFAULT_AUTH_RESPONSE, DEFAULT_COLLABORATORS_RESPONSE, DEFAULT_COMMENT_RESPONSE, DEFAULT_COMMENTS_RESPONSE, DEFAULT_COMPLETED_TASKS_RESPONSE, DEFAULT_LABEL_RESPONSE, DEFAULT_LABELS_RESPONSE, DEFAULT_PROJECT_RESPONSE, DEFAULT_PROJECTS_RESPONSE, DEFAULT_SECTION_RESPONSE, DEFAULT_SECTIONS_RESPONSE, DEFAULT_TASK_META_RESPONSE, DEFAULT_TASK_RESPONSE, DEFAULT_TASKS_RESPONSE, DEFAULT_TOKEN, PaginatedItems, PaginatedResults, ) from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import ( AuthResult, Collaborator, Comment, Label, Project, Section, Task, ) if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture def requests_mock() -> Iterator[responses.RequestsMock]: with responses.RequestsMock() as requests_mock: yield requests_mock @pytest.fixture def todoist_api() -> TodoistAPI: return TodoistAPI(DEFAULT_TOKEN) @pytest.fixture def todoist_api_async() -> TodoistAPIAsync: return TodoistAPIAsync(DEFAULT_TOKEN) @pytest.fixture def default_task_response() -> dict[str, Any]: return DEFAULT_TASK_RESPONSE @pytest.fixture def default_task_meta() -> Task: return Task.from_dict(DEFAULT_TASK_META_RESPONSE) @pytest.fixture def default_task_meta_response() -> dict[str, Any]: return DEFAULT_TASK_META_RESPONSE @pytest.fixture def default_task() -> Task: return Task.from_dict(DEFAULT_TASK_RESPONSE) @pytest.fixture def default_tasks_response() -> list[PaginatedResults]: return DEFAULT_TASKS_RESPONSE @pytest.fixture def default_tasks_list() -> list[list[Task]]: return [ [Task.from_dict(result) for result in response["results"]] for response in DEFAULT_TASKS_RESPONSE ] @pytest.fixture def default_completed_tasks_response() -> list[PaginatedItems]: return DEFAULT_COMPLETED_TASKS_RESPONSE @pytest.fixture def default_completed_tasks_list() -> list[list[Task]]: return [ [Task.from_dict(result) for result in response["items"]] for response in DEFAULT_COMPLETED_TASKS_RESPONSE ] @pytest.fixture def default_project_response() -> dict[str, Any]: return DEFAULT_PROJECT_RESPONSE @pytest.fixture def default_project() -> Project: return Project.from_dict(DEFAULT_PROJECT_RESPONSE) @pytest.fixture def default_projects_response() -> list[PaginatedResults]: return DEFAULT_PROJECTS_RESPONSE @pytest.fixture def default_projects_list() -> list[list[Project]]: return [ [Project.from_dict(result) for result in response["results"]] for response in DEFAULT_PROJECTS_RESPONSE ] @pytest.fixture def default_collaborators_response() -> list[PaginatedResults]: return DEFAULT_COLLABORATORS_RESPONSE @pytest.fixture def default_collaborators_list() -> list[list[Collaborator]]: return [ [Collaborator.from_dict(result) for result in response["results"]] for response in DEFAULT_COLLABORATORS_RESPONSE ] @pytest.fixture def default_section_response() -> dict[str, Any]: return DEFAULT_SECTION_RESPONSE @pytest.fixture def default_section() -> Section: return Section.from_dict(DEFAULT_SECTION_RESPONSE) @pytest.fixture def default_sections_response() -> list[PaginatedResults]: return DEFAULT_SECTIONS_RESPONSE @pytest.fixture def default_sections_list() -> list[list[Section]]: return [ [Section.from_dict(result) for result in response["results"]] for response in DEFAULT_SECTIONS_RESPONSE ] @pytest.fixture def default_comment_response() -> dict[str, Any]: return DEFAULT_COMMENT_RESPONSE @pytest.fixture def default_comment() -> Comment: return Comment.from_dict(DEFAULT_COMMENT_RESPONSE) @pytest.fixture def default_comments_response() -> list[PaginatedResults]: return DEFAULT_COMMENTS_RESPONSE @pytest.fixture def default_comments_list() -> list[list[Comment]]: return [ [Comment.from_dict(result) for result in response["results"]] for response in DEFAULT_COMMENTS_RESPONSE ] @pytest.fixture def default_label_response() -> dict[str, Any]: return DEFAULT_LABEL_RESPONSE @pytest.fixture def default_label() -> Label: return Label.from_dict(DEFAULT_LABEL_RESPONSE) @pytest.fixture def default_labels_response() -> list[PaginatedResults]: return DEFAULT_LABELS_RESPONSE @pytest.fixture def default_labels_list() -> list[list[Label]]: return [ [Label.from_dict(result) for result in response["results"]] for response in DEFAULT_LABELS_RESPONSE ] @pytest.fixture def default_quick_add_response() -> dict[str, Any]: return DEFAULT_TASK_RESPONSE @pytest.fixture def default_quick_add_result() -> Task: return Task.from_dict(DEFAULT_TASK_RESPONSE) @pytest.fixture def default_auth_response() -> dict[str, Any]: return DEFAULT_AUTH_RESPONSE @pytest.fixture def default_auth_result() -> AuthResult: return AuthResult.from_dict(DEFAULT_AUTH_RESPONSE) todoist-api-python-3.1.0/tests/data/000077500000000000000000000000001500671641500173465ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/data/__init__.py000066400000000000000000000000001500671641500214450ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/data/test_defaults.py000066400000000000000000000164021500671641500225710ustar00rootroot00000000000000from __future__ import annotations from typing import Any, TypedDict class PaginatedResults(TypedDict): results: list[dict[str, Any]] next_cursor: str | None class PaginatedItems(TypedDict): items: list[dict[str, Any]] next_cursor: str | None DEFAULT_API_URL = "https://api.todoist.com/api/v1" DEFAULT_OAUTH_URL = "https://todoist.com/oauth" DEFAULT_TOKEN = "some-default-token" DEFAULT_REQUEST_ID = "f00dbeef-cafe-4bad-a555-deadc0decafe" DEFAULT_DUE_RESPONSE = { "date": "2016-09-01", "timezone": "Europe/Moscow", "string": "tomorrow at 12", "lang": "en", "is_recurring": True, } DEFAULT_DEADLINE_RESPONSE = { "date": "2016-09-01", "lang": "en", } DEFAULT_DURATION_RESPONSE = { "amount": 60, "unit": "minute", } DEFAULT_META_RESPONSE: dict[str, Any] = { "project": ["6X7rM8997g3RQmvh", "Inbox"], "section": [None, None], "assignee": [None, None], "labels": {}, "due": None, "deadline": None, } DEFAULT_PROJECT_RESPONSE = { "id": "6X7rM8997g3RQmvh", "name": "Inbox", "description": "", "parent_id": "6X7rfFVPjhvv84XG", "folder_id": None, "workspace_id": None, "child_order": 1, "color": "red", "shared": False, "collapsed": False, "is_favorite": False, "is_inbox_project": True, "can_assign_tasks": False, "is_archived": False, "view_style": "list", "created_at": "2023-02-01T00:00:00.000000Z", "updated_at": "2025-04-03T03:14:15.926536Z", } DEFAULT_PROJECT_RESPONSE_2 = dict(DEFAULT_PROJECT_RESPONSE) DEFAULT_PROJECT_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG" DEFAULT_PROJECT_RESPONSE_2["is_inbox_project"] = False DEFAULT_PROJECT_RESPONSE_3 = dict(DEFAULT_PROJECT_RESPONSE) DEFAULT_PROJECT_RESPONSE_3["id"] = "6X7rfEVP8hvv25ZQ" DEFAULT_PROJECT_RESPONSE_3["is_inbox_project"] = False DEFAULT_PROJECTS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_PROJECT_RESPONSE, DEFAULT_PROJECT_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_PROJECT_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_TASK_RESPONSE: dict[str, Any] = { "id": "6X7rM8997g3RQmvh", "content": "Some task content", "description": "Some task description", "project_id": "6Jf8VQXxpwv56VQ7", "section_id": "3Ty8VQXxpwv28PK3", "parent_id": "6X7rf9x6pv2FGghW", "labels": [], "priority": 1, "due": DEFAULT_DUE_RESPONSE, "deadline": DEFAULT_DEADLINE_RESPONSE, "duration": DEFAULT_DURATION_RESPONSE, "collapsed": False, "child_order": 3, "responsible_uid": "2423523", "assigned_by_uid": "2971358", "completed_at": None, "added_by_uid": "34567", "added_at": "2014-09-26T08:25:05.000000Z", "updated_at": "2016-01-02T21:00:30.000000Z", } DEFAULT_TASK_RESPONSE_2 = dict(DEFAULT_TASK_RESPONSE) DEFAULT_TASK_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG" DEFAULT_TASK_RESPONSE_3 = dict(DEFAULT_TASK_RESPONSE) DEFAULT_TASK_RESPONSE_3["id"] = "6X7rF9xvX25jTxm5" DEFAULT_TASKS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_TASK_RESPONSE, DEFAULT_TASK_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_TASK_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_TASK_META_RESPONSE = dict(DEFAULT_TASK_RESPONSE) DEFAULT_TASK_META_RESPONSE["meta"] = DEFAULT_META_RESPONSE DEFAULT_COMPLETED_TASK_RESPONSE = dict(DEFAULT_TASK_RESPONSE) DEFAULT_COMPLETED_TASK_RESPONSE["completed_at"] = "2024-02-13T10:00:00.000000Z" DEFAULT_COMPLETED_TASK_RESPONSE_2 = dict(DEFAULT_COMPLETED_TASK_RESPONSE) DEFAULT_COMPLETED_TASK_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG" DEFAULT_COMPLETED_TASK_RESPONSE_3 = dict(DEFAULT_COMPLETED_TASK_RESPONSE) DEFAULT_COMPLETED_TASK_RESPONSE_3["id"] = "6X7rfEVP8hvv25ZQ" DEFAULT_COMPLETED_TASKS_RESPONSE: list[PaginatedItems] = [ { "items": [ DEFAULT_COMPLETED_TASK_RESPONSE, DEFAULT_COMPLETED_TASK_RESPONSE_2, ], "next_cursor": "next", }, { "items": [DEFAULT_COMPLETED_TASK_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_COLLABORATOR_RESPONSE = { "id": "6X7rM8997g3RQmvh", "name": "Alice", "email": "alice@example.com", } DEFAULT_COLLABORATOR_RESPONSE_2 = dict(DEFAULT_COLLABORATOR_RESPONSE) DEFAULT_COLLABORATOR_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG" DEFAULT_COLLABORATOR_RESPONSE_3 = dict(DEFAULT_COLLABORATOR_RESPONSE) DEFAULT_COLLABORATOR_RESPONSE_3["id"] = "6X7rjKtP98vG84rK" DEFAULT_COLLABORATORS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_COLLABORATOR_RESPONSE, DEFAULT_COLLABORATOR_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_COLLABORATOR_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_SECTION_RESPONSE = { "id": "6X7rM8997g3RQmvh", "project_id": "4567", "name": "A Section", "collapsed": False, "order": 1, } DEFAULT_SECTION_RESPONSE_2 = dict(DEFAULT_SECTION_RESPONSE) DEFAULT_SECTION_RESPONSE_2["id"] = "6X7FxXvX84jHphx" DEFAULT_SECTION_RESPONSE_3 = dict(DEFAULT_SECTION_RESPONSE) DEFAULT_SECTION_RESPONSE_3["id"] = "6X7rF9xvX25jTzm7" DEFAULT_SECTIONS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_SECTION_RESPONSE, DEFAULT_SECTION_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_SECTION_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_ATTACHMENT_RESPONSE = { "resource_type": "file", "file_name": "File.pdf", "file_type": "application/pdf", "file_size": 4321, "file_url": "https://cdn-domain.tld/path/to/file.pdf", "upload_state": "completed", "image": "https://cdn-domain.tld/path/to/some_image.jpg", "image_width": 800, "image_height": 600, "url": "https://todoist.com", "title": "Todoist Website", } DEFAULT_COMMENT_RESPONSE: dict[str, Any] = { "id": "6X7rM8997g3RQmvh", "content": "A comment", "posted_uid": "34567", "posted_at": "2019-09-22T07:00:00.000000Z", "task_id": "6X7rM8997g3RQmvh", "project_id": "6X7rfEVP8hvv25ZQ", "attachment": DEFAULT_ATTACHMENT_RESPONSE, } DEFAULT_COMMENT_RESPONSE_2 = dict(DEFAULT_COMMENT_RESPONSE) DEFAULT_COMMENT_RESPONSE_2["id"] = "6X7rfFVPjhvv84XG" DEFAULT_COMMENT_RESPONSE_2["attachment"] = None DEFAULT_COMMENT_RESPONSE_3 = dict(DEFAULT_COMMENT_RESPONSE) DEFAULT_COMMENT_RESPONSE_3["id"] = "6X7rfFVPjhvv65HG" DEFAULT_COMMENT_RESPONSE_3["attachment"] = None DEFAULT_COMMENTS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_COMMENT_RESPONSE, DEFAULT_COMMENT_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_COMMENT_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_LABEL_RESPONSE = { "id": "1234", "name": "A label", "color": "red", "order": 1, "is_favorite": True, } DEFAULT_LABEL_RESPONSE_2 = dict(DEFAULT_LABEL_RESPONSE) DEFAULT_LABEL_RESPONSE_2["id"] = "4567" DEFAULT_LABEL_RESPONSE_3 = dict(DEFAULT_LABEL_RESPONSE) DEFAULT_LABEL_RESPONSE_3["id"] = "6789" DEFAULT_LABELS_RESPONSE: list[PaginatedResults] = [ { "results": [DEFAULT_LABEL_RESPONSE, DEFAULT_LABEL_RESPONSE_2], "next_cursor": "next", }, { "results": [DEFAULT_LABEL_RESPONSE_3], "next_cursor": None, }, ] DEFAULT_AUTH_RESPONSE = { "access_token": "123456789", "state": "somestate", } todoist-api-python-3.1.0/tests/test_api_comments.py000066400000000000000000000132431500671641500225270ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import ( DEFAULT_API_URL, PaginatedResults, ) from tests.utils.test_utils import ( auth_matcher, data_matcher, enumerate_async, param_matcher, request_id_matcher, ) from todoist_api_python.models import Attachment if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Comment @pytest.mark.asyncio async def test_get_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_comment_response: dict[str, Any], default_comment: Comment, ) -> None: comment_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/comments/{comment_id}" requests_mock.add( method=responses.GET, url=endpoint, json=default_comment_response, status=200, match=[auth_matcher(), request_id_matcher()], ) comment = todoist_api.get_comment(comment_id) assert len(requests_mock.calls) == 1 assert comment == default_comment comment = await todoist_api_async.get_comment(comment_id) assert len(requests_mock.calls) == 2 assert comment == default_comment @pytest.mark.asyncio async def test_get_comments( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_comments_response: list[PaginatedResults], default_comments_list: list[list[Comment]], ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/comments" cursor: str | None = None for page in default_comments_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[ auth_matcher(), request_id_matcher(), param_matcher({"task_id": task_id}, cursor), ], ) cursor = page["next_cursor"] count = 0 comments_iter = todoist_api.get_comments(task_id=task_id) for i, comments in enumerate(comments_iter): assert len(requests_mock.calls) == count + 1 assert comments == default_comments_list[i] count += 1 comments_async_iter = await todoist_api_async.get_comments(task_id=task_id) async for i, comments in enumerate_async(comments_async_iter): assert len(requests_mock.calls) == count + 1 assert comments == default_comments_list[i] count += 1 @pytest.mark.asyncio async def test_add_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_comment_response: dict[str, Any], default_comment: Comment, ) -> None: content = "A Comment" project_id = "6HWcc9PJCvPjCxC9" attachment = Attachment( resource_type="file", file_url="https://s3.amazonaws.com/domorebetter/Todoist+Setup+Guide.pdf", file_type="application/pdf", file_name="File.pdf", ) requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/comments", json=default_comment_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher( { "content": content, "project_id": project_id, "attachment": attachment.to_dict(), } ), ], ) new_comment = todoist_api.add_comment( content=content, project_id=project_id, attachment=attachment, ) assert len(requests_mock.calls) == 1 assert new_comment == default_comment new_comment = await todoist_api_async.add_comment( content=content, project_id=project_id, attachment=attachment, ) assert len(requests_mock.calls) == 2 assert new_comment == default_comment @pytest.mark.asyncio async def test_update_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_comment: Comment, ) -> None: args = { "content": "An updated comment", } updated_comment_dict = default_comment.to_dict() | args requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/comments/{default_comment.id}", json=updated_comment_dict, status=200, match=[auth_matcher(), request_id_matcher(), data_matcher(args)], ) response = todoist_api.update_comment(comment_id=default_comment.id, **args) assert len(requests_mock.calls) == 1 assert response == Comment.from_dict(updated_comment_dict) response = await todoist_api_async.update_comment( comment_id=default_comment.id, **args ) assert len(requests_mock.calls) == 2 assert response == Comment.from_dict(updated_comment_dict) @pytest.mark.asyncio async def test_delete_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: comment_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/comments/{comment_id}" requests_mock.add( method=responses.DELETE, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.delete_comment(comment_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.delete_comment(comment_id) assert len(requests_mock.calls) == 2 assert response is True todoist-api-python-3.1.0/tests/test_api_completed_tasks.py000066400000000000000000000104121500671641500240560ustar00rootroot00000000000000from __future__ import annotations import sys from datetime import datetime, timezone from typing import TYPE_CHECKING, Any if sys.version_info >= (3, 11): from datetime import UTC else: UTC = timezone.utc import pytest import responses from tests.data.test_defaults import DEFAULT_API_URL, PaginatedItems from tests.utils.test_utils import ( auth_matcher, enumerate_async, param_matcher, request_id_matcher, ) from todoist_api_python._core.utils import format_datetime if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Task @pytest.mark.asyncio async def test_get_completed_tasks_by_due_date( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_completed_tasks_response: list[PaginatedItems], default_completed_tasks_list: list[list[Task]], ) -> None: since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC) until = datetime(2024, 2, 1, 0, 0, 0, tzinfo=UTC) project_id = "6X7rM8997g3RQmvh" filter_query = "p1" params = { "since": format_datetime(since), "until": format_datetime(until), "project_id": project_id, "filter_query": filter_query, } endpoint = f"{DEFAULT_API_URL}/tasks/completed/by_due_date" cursor: str | None = None for page in default_completed_tasks_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)], ) cursor = page["next_cursor"] count = 0 tasks_iter = todoist_api.get_completed_tasks_by_due_date( since=since, until=until, project_id=project_id, filter_query=filter_query, ) for i, tasks in enumerate(tasks_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_completed_tasks_list[i] count += 1 tasks_async_iter = await todoist_api_async.get_completed_tasks_by_due_date( since=since, until=until, project_id=project_id, filter_query=filter_query, ) async for i, tasks in enumerate_async(tasks_async_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_completed_tasks_list[i] count += 1 @pytest.mark.asyncio async def test_get_completed_tasks_by_completion_date( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_completed_tasks_response: list[PaginatedItems], default_completed_tasks_list: list[list[Task]], ) -> None: since = datetime(2024, 3, 1, 0, 0, 0) # noqa: DTZ001 until = datetime(2024, 4, 1, 0, 0, 0) # noqa: DTZ001 workspace_id = "123" filter_query = "@label" params: dict[str, Any] = { "since": format_datetime(since), "until": format_datetime(until), "workspace_id": workspace_id, "filter_query": filter_query, } endpoint = f"{DEFAULT_API_URL}/tasks/completed/by_completion_date" cursor: str | None = None for page in default_completed_tasks_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)], ) cursor = page["next_cursor"] count = 0 tasks_iter = todoist_api.get_completed_tasks_by_completion_date( since=since, until=until, workspace_id=workspace_id, filter_query=filter_query, ) for i, tasks in enumerate(tasks_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_completed_tasks_list[i] count += 1 tasks_async_iter = await todoist_api_async.get_completed_tasks_by_completion_date( since=since, until=until, workspace_id=workspace_id, filter_query=filter_query, ) async for i, tasks in enumerate_async(tasks_async_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_completed_tasks_list[i] count += 1 todoist-api-python-3.1.0/tests/test_api_labels.py000066400000000000000000000132231500671641500221420ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_API_URL, PaginatedResults from tests.utils.test_utils import ( auth_matcher, data_matcher, enumerate_async, param_matcher, request_id_matcher, ) if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Label @pytest.mark.asyncio async def test_get_label( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_label_response: dict[str, Any], default_label: Label, ) -> None: label_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/labels/{label_id}" requests_mock.add( method=responses.GET, url=endpoint, json=default_label_response, status=200, match=[auth_matcher()], ) label = todoist_api.get_label(label_id) assert len(requests_mock.calls) == 1 assert label == default_label label = await todoist_api_async.get_label(label_id) assert len(requests_mock.calls) == 2 assert label == default_label @pytest.mark.asyncio async def test_get_labels( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_labels_response: list[PaginatedResults], default_labels_list: list[list[Label]], ) -> None: endpoint = f"{DEFAULT_API_URL}/labels" cursor: str | None = None for page in default_labels_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher({}, cursor)], ) cursor = page["next_cursor"] count = 0 labels_iter = todoist_api.get_labels() for i, labels in enumerate(labels_iter): assert len(requests_mock.calls) == count + 1 assert labels == default_labels_list[i] count += 1 labels_async_iter = await todoist_api_async.get_labels() async for i, labels in enumerate_async(labels_async_iter): assert len(requests_mock.calls) == count + 1 assert labels == default_labels_list[i] count += 1 @pytest.mark.asyncio async def test_add_label_minimal( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_label_response: dict[str, Any], default_label: Label, ) -> None: label_name = "A Label" requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/labels", json=default_label_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"name": label_name}), ], ) new_label = todoist_api.add_label(name=label_name) assert len(requests_mock.calls) == 1 assert new_label == default_label new_label = await todoist_api_async.add_label(name=label_name) assert len(requests_mock.calls) == 2 assert new_label == default_label @pytest.mark.asyncio async def test_add_label_full( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_label_response: dict[str, Any], default_label: Label, ) -> None: label_name = "A Label" args: dict[str, Any] = { "color": "red", "item_order": 3, "is_favorite": True, } requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/labels", json=default_label_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"name": label_name} | args), ], ) new_label = todoist_api.add_label(name=label_name, **args) assert len(requests_mock.calls) == 1 assert new_label == default_label new_label = await todoist_api_async.add_label(name=label_name, **args) assert len(requests_mock.calls) == 2 assert new_label == default_label @pytest.mark.asyncio async def test_update_label( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_label: Label, ) -> None: args: dict[str, Any] = { "name": "An updated label", } updated_label_dict = default_label.to_dict() | args requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/labels/{default_label.id}", json=updated_label_dict, status=200, match=[auth_matcher(), request_id_matcher(), data_matcher(args)], ) response = todoist_api.update_label(label_id=default_label.id, **args) assert len(requests_mock.calls) == 1 assert response == Label.from_dict(updated_label_dict) response = await todoist_api_async.update_label(label_id=default_label.id, **args) assert len(requests_mock.calls) == 2 assert response == Label.from_dict(updated_label_dict) @pytest.mark.asyncio async def test_delete_label( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: label_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/labels/{label_id}" requests_mock.add( method=responses.DELETE, url=endpoint, status=204, ) response = todoist_api.delete_label(label_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.delete_label(label_id) assert len(requests_mock.calls) == 2 assert response is True todoist-api-python-3.1.0/tests/test_api_projects.py000066400000000000000000000217641500671641500225420ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_API_URL, PaginatedResults from tests.utils.test_utils import ( auth_matcher, data_matcher, enumerate_async, param_matcher, request_id_matcher, ) if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Collaborator, Project @pytest.mark.asyncio async def test_get_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project_response: dict[str, Any], default_project: Project, ) -> None: project_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/projects/{project_id}" requests_mock.add( method=responses.GET, url=endpoint, json=default_project_response, status=200, match=[auth_matcher()], ) project = todoist_api.get_project(project_id) assert len(requests_mock.calls) == 1 assert project == default_project project = await todoist_api_async.get_project(project_id) assert len(requests_mock.calls) == 2 assert project == default_project @pytest.mark.asyncio async def test_get_projects( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_projects_response: list[PaginatedResults], default_projects_list: list[list[Project]], ) -> None: endpoint = f"{DEFAULT_API_URL}/projects" cursor: str | None = None for page in default_projects_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher({}, cursor)], ) cursor = page["next_cursor"] count = 0 projects_iter = todoist_api.get_projects() for i, projects in enumerate(projects_iter): assert len(requests_mock.calls) == count + 1 assert projects == default_projects_list[i] count += 1 projects_async_iter = await todoist_api_async.get_projects() async for i, projects in enumerate_async(projects_async_iter): assert len(requests_mock.calls) == count + 1 assert projects == default_projects_list[i] count += 1 @pytest.mark.asyncio async def test_add_project_minimal( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project_response: dict[str, Any], default_project: Project, ) -> None: project_name = "A Project" requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/projects", json=default_project_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"name": project_name}), ], ) new_project = todoist_api.add_project(name=project_name) assert len(requests_mock.calls) == 1 assert new_project == default_project new_project = await todoist_api_async.add_project(name=project_name) assert len(requests_mock.calls) == 2 assert new_project == default_project @pytest.mark.asyncio async def test_add_project_full( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project_response: dict[str, Any], default_project: Project, ) -> None: project_name = "A Project" requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/projects", json=default_project_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"name": project_name}), ], ) new_project = todoist_api.add_project(name=project_name) assert len(requests_mock.calls) == 1 assert new_project == default_project new_project = await todoist_api_async.add_project(name=project_name) assert len(requests_mock.calls) == 2 assert new_project == default_project @pytest.mark.asyncio async def test_update_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project: Project, ) -> None: args: dict[str, Any] = { "name": "An updated project", "color": "red", "is_favorite": False, } updated_project_dict = default_project.to_dict() | args requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/projects/{default_project.id}", json=updated_project_dict, status=200, match=[auth_matcher(), request_id_matcher(), data_matcher(args)], ) response = todoist_api.update_project(project_id=default_project.id, **args) assert len(requests_mock.calls) == 1 assert response == Project.from_dict(updated_project_dict) response = await todoist_api_async.update_project( project_id=default_project.id, **args ) assert len(requests_mock.calls) == 2 assert response == Project.from_dict(updated_project_dict) @pytest.mark.asyncio async def test_archive_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project: Project, ) -> None: project_id = default_project.id endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/archive" archived_project_dict = default_project.to_dict() archived_project_dict["is_archived"] = True requests_mock.add( method=responses.POST, url=endpoint, json=archived_project_dict, status=200, match=[auth_matcher(), request_id_matcher()], ) project = todoist_api.archive_project(project_id) assert len(requests_mock.calls) == 1 assert project == Project.from_dict(archived_project_dict) project = await todoist_api_async.archive_project(project_id) assert len(requests_mock.calls) == 2 assert project == Project.from_dict(archived_project_dict) @pytest.mark.asyncio async def test_unarchive_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_project: Project, ) -> None: project_id = default_project.id endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/unarchive" unarchived_project_dict = default_project.to_dict() unarchived_project_dict["is_archived"] = False requests_mock.add( method=responses.POST, url=endpoint, json=unarchived_project_dict, status=200, match=[auth_matcher(), request_id_matcher()], ) project = todoist_api.unarchive_project(project_id) assert len(requests_mock.calls) == 1 assert project == Project.from_dict(unarchived_project_dict) project = await todoist_api_async.unarchive_project(project_id) assert len(requests_mock.calls) == 2 assert project == Project.from_dict(unarchived_project_dict) @pytest.mark.asyncio async def test_delete_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: project_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/projects/{project_id}" requests_mock.add( method=responses.DELETE, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.delete_project(project_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.delete_project(project_id) assert len(requests_mock.calls) == 2 assert response is True @pytest.mark.asyncio async def test_get_collaborators( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_collaborators_response: list[PaginatedResults], default_collaborators_list: list[list[Collaborator]], ) -> None: project_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/collaborators" cursor: str | None = None for page in default_collaborators_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher({}, cursor)], ) cursor = page["next_cursor"] count = 0 collaborators_iter = todoist_api.get_collaborators(project_id) for i, collaborators in enumerate(collaborators_iter): assert len(requests_mock.calls) == count + 1 assert collaborators == default_collaborators_list[i] count += 1 collaborators_async_iter = await todoist_api_async.get_collaborators(project_id) async for i, collaborators in enumerate_async(collaborators_async_iter): assert len(requests_mock.calls) == count + 1 assert collaborators == default_collaborators_list[i] count += 1 todoist-api-python-3.1.0/tests/test_api_sections.py000066400000000000000000000145511500671641500225340ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_API_URL, PaginatedResults from tests.utils.test_utils import ( auth_matcher, data_matcher, enumerate_async, param_matcher, request_id_matcher, ) if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Section @pytest.mark.asyncio async def test_get_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_section_response: dict[str, Any], default_section: Section, ) -> None: section_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/sections/{section_id}" requests_mock.add( method=responses.GET, url=endpoint, json=default_section_response, status=200, match=[auth_matcher()], ) section = todoist_api.get_section(section_id) assert len(requests_mock.calls) == 1 assert section == default_section section = await todoist_api_async.get_section(section_id) assert len(requests_mock.calls) == 2 assert section == default_section @pytest.mark.asyncio async def test_get_sections( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_sections_response: list[PaginatedResults], default_sections_list: list[list[Section]], ) -> None: endpoint = f"{DEFAULT_API_URL}/sections" cursor: str | None = None for page in default_sections_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher({}, cursor)], ) cursor = page["next_cursor"] count = 0 sections_iter = todoist_api.get_sections() for i, sections in enumerate(sections_iter): assert len(requests_mock.calls) == count + 1 assert sections == default_sections_list[i] count += 1 sections_async_iter = await todoist_api_async.get_sections() async for i, sections in enumerate_async(sections_async_iter): assert len(requests_mock.calls) == count + 1 assert sections == default_sections_list[i] count += 1 @pytest.mark.asyncio async def test_get_sections_by_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_sections_response: list[PaginatedResults], default_sections_list: list[list[Section]], ) -> None: project_id = "123" endpoint = f"{DEFAULT_API_URL}/sections" cursor: str | None = None for page in default_sections_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[ auth_matcher(), request_id_matcher(), param_matcher({"project_id": project_id}, cursor), ], ) cursor = page["next_cursor"] count = 0 sections_iter = todoist_api.get_sections(project_id=project_id) for i, sections in enumerate(sections_iter): assert len(requests_mock.calls) == count + 1 assert sections == default_sections_list[i] count += 1 sections_async_iter = await todoist_api_async.get_sections(project_id=project_id) async for i, sections in enumerate_async(sections_async_iter): assert len(requests_mock.calls) == count + 1 assert sections == default_sections_list[i] count += 1 @pytest.mark.asyncio async def test_add_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_section_response: dict[str, Any], default_section: Section, ) -> None: section_name = "A Section" project_id = "123" args = { "order": 3, } requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/sections", json=default_section_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"name": section_name, "project_id": project_id} | args), ], ) new_section = todoist_api.add_section( name=section_name, project_id=project_id, **args ) assert len(requests_mock.calls) == 1 assert new_section == default_section new_section = await todoist_api_async.add_section( name=section_name, project_id=project_id, **args ) assert len(requests_mock.calls) == 2 assert new_section == default_section @pytest.mark.asyncio async def test_update_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_section: Section, ) -> None: args = { "name": "An updated section", } updated_section_dict = default_section.to_dict() | args requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/sections/{default_section.id}", json=updated_section_dict, status=200, match=[auth_matcher(), request_id_matcher(), data_matcher(args)], ) response = todoist_api.update_section(section_id=default_section.id, **args) assert len(requests_mock.calls) == 1 assert response == Section.from_dict(updated_section_dict) response = await todoist_api_async.update_section( section_id=default_section.id, **args ) assert len(requests_mock.calls) == 2 assert response == Section.from_dict(updated_section_dict) @pytest.mark.asyncio async def test_delete_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: section_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/sections/{section_id}" requests_mock.add( method=responses.DELETE, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.delete_section(section_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.delete_section(section_id) assert len(requests_mock.calls) == 2 assert response is True todoist-api-python-3.1.0/tests/test_api_tasks.py000066400000000000000000000322261500671641500220310ustar00rootroot00000000000000from __future__ import annotations import sys from datetime import datetime, timezone from typing import TYPE_CHECKING, Any if sys.version_info >= (3, 11): from datetime import UTC else: UTC = timezone.utc import pytest import responses from tests.data.test_defaults import DEFAULT_API_URL, PaginatedResults from tests.utils.test_utils import ( auth_matcher, data_matcher, enumerate_async, param_matcher, request_id_matcher, ) if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import Task @pytest.mark.asyncio async def test_get_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_task_response: dict[str, Any], default_task: Task, ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}" requests_mock.add( method=responses.GET, url=endpoint, json=default_task_response, match=[auth_matcher(), request_id_matcher()], ) task = todoist_api.get_task(task_id) assert len(requests_mock.calls) == 1 assert task == default_task task = await todoist_api_async.get_task(task_id) assert len(requests_mock.calls) == 2 assert task == default_task @pytest.mark.asyncio async def test_get_tasks( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_tasks_response: list[PaginatedResults], default_tasks_list: list[list[Task]], ) -> None: endpoint = f"{DEFAULT_API_URL}/tasks" cursor: str | None = None for page in default_tasks_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher({}, cursor)], ) cursor = page["next_cursor"] count = 0 tasks_iter = todoist_api.get_tasks() for i, tasks in enumerate(tasks_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 tasks_async_iter = await todoist_api_async.get_tasks() async for i, tasks in enumerate_async(tasks_async_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 @pytest.mark.asyncio async def test_get_tasks_with_filters( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_tasks_response: list[PaginatedResults], default_tasks_list: list[list[Task]], ) -> None: project_id = "123" section_id = "456" parent_id = "789" label = "test-label" ids = ["1", "2", "3"] limit = 30 params: dict[str, Any] = { "project_id": project_id, "section_id": section_id, "parent_id": parent_id, "label": label, "ids": ",".join(ids), "limit": limit, } endpoint = f"{DEFAULT_API_URL}/tasks" cursor: str | None = None for page in default_tasks_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)], ) cursor = page["next_cursor"] count = 0 tasks_iter = todoist_api.get_tasks( project_id=project_id, section_id=section_id, parent_id=parent_id, label=label, ids=ids, limit=limit, ) for i, tasks in enumerate(tasks_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 tasks_async_iter = await todoist_api_async.get_tasks( project_id=project_id, section_id=section_id, parent_id=parent_id, label=label, ids=ids, limit=limit, ) async for i, tasks in enumerate_async(tasks_async_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 @pytest.mark.asyncio async def test_filter_tasks( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_tasks_response: list[PaginatedResults], default_tasks_list: list[list[Task]], ) -> None: query = "today or overdue" lang = "en" params = { "query": "today or overdue", "lang": "en", } endpoint = f"{DEFAULT_API_URL}/tasks/filter" cursor: str | None = None for page in default_tasks_response: requests_mock.add( method=responses.GET, url=endpoint, json=page, status=200, match=[auth_matcher(), request_id_matcher(), param_matcher(params, cursor)], ) cursor = page["next_cursor"] count = 0 tasks_iter = todoist_api.filter_tasks( query=query, lang=lang, ) for i, tasks in enumerate(tasks_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 # Test async iterator tasks_async_iter = await todoist_api_async.filter_tasks( query=query, lang=lang, ) async for i, tasks in enumerate_async(tasks_async_iter): assert len(requests_mock.calls) == count + 1 assert tasks == default_tasks_list[i] count += 1 @pytest.mark.asyncio async def test_add_task_minimal( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_task_response: dict[str, Any], default_task: Task, ) -> None: content = "Some content" requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/tasks", json=default_task_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher({"content": content}), ], ) new_task = todoist_api.add_task(content=content) assert len(requests_mock.calls) == 1 assert new_task == default_task new_task = await todoist_api_async.add_task(content=content) assert len(requests_mock.calls) == 2 assert new_task == default_task @pytest.mark.asyncio async def test_add_task_full( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_task_response: dict[str, Any], default_task: Task, ) -> None: content = "Some content" due_datetime = datetime(2021, 1, 1, 11, 0, 0, tzinfo=UTC) args: dict[str, Any] = { "description": "A description", "project_id": "123", "section_id": "456", "parent_id": "789", "labels": ["label1", "label2"], "priority": 4, "due_string": "today", "due_lang": "en", "assignee_id": "321", "order": 3, "auto_reminder": True, "auto_parse_labels": True, "duration": 60, "duration_unit": "minute", } requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/tasks", json=default_task_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher( { "content": content, "due_datetime": due_datetime.strftime("%Y-%m-%dT%H:%M:%SZ"), } | args ), ], ) new_task = todoist_api.add_task(content=content, due_datetime=due_datetime, **args) assert len(requests_mock.calls) == 1 assert new_task == default_task new_task = await todoist_api_async.add_task( content=content, due_datetime=due_datetime, **args ) assert len(requests_mock.calls) == 2 assert new_task == default_task @pytest.mark.asyncio async def test_add_task_quick( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_task_meta_response: dict[str, Any], default_task_meta: Task, ) -> None: text = "Buy milk tomorrow at 9am #Shopping @errands" note = "Whole milk x6" auto_reminder = True requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/tasks/quick", json=default_task_meta_response, status=200, match=[ auth_matcher(), request_id_matcher(), data_matcher( { "meta": True, "text": text, "auto_reminder": auto_reminder, "note": note, } ), ], ) task = todoist_api.add_task_quick( text=text, note=note, auto_reminder=auto_reminder, ) assert len(requests_mock.calls) == 1 assert task == default_task_meta task = await todoist_api_async.add_task_quick( text=text, note=note, auto_reminder=auto_reminder, ) assert len(requests_mock.calls) == 2 assert task == default_task_meta @pytest.mark.asyncio async def test_update_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_task: Task, ) -> None: args: dict[str, Any] = { "content": "Updated content", "description": "Updated description", "labels": ["label1", "label2"], "priority": 2, } updated_task_dict = default_task.to_dict() | args requests_mock.add( method=responses.POST, url=f"{DEFAULT_API_URL}/tasks/{default_task.id}", json=updated_task_dict, status=200, match=[auth_matcher(), request_id_matcher(), data_matcher(args)], ) response = todoist_api.update_task(task_id=default_task.id, **args) assert len(requests_mock.calls) == 1 assert response == Task.from_dict(updated_task_dict) response = await todoist_api_async.update_task(task_id=default_task.id, **args) assert len(requests_mock.calls) == 2 assert response == Task.from_dict(updated_task_dict) @pytest.mark.asyncio async def test_complete_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}/close" requests_mock.add( method=responses.POST, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.complete_task(task_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.complete_task(task_id) assert len(requests_mock.calls) == 2 assert response is True @pytest.mark.asyncio async def test_uncomplete_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}/reopen" requests_mock.add( method=responses.POST, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.uncomplete_task(task_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.uncomplete_task(task_id) assert len(requests_mock.calls) == 2 assert response is True @pytest.mark.asyncio async def test_move_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}/move" requests_mock.add( method=responses.POST, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.move_task(task_id, project_id="123") assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.move_task(task_id, section_id="456") assert len(requests_mock.calls) == 2 assert response is True response = await todoist_api_async.move_task(task_id, parent_id="789") assert len(requests_mock.calls) == 3 assert response is True with pytest.raises( ValueError, match="Either `project_id`, `section_id`, or `parent_id` must be provided.", ): response = await todoist_api_async.move_task(task_id) @pytest.mark.asyncio async def test_delete_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ) -> None: task_id = "6X7rM8997g3RQmvh" endpoint = f"{DEFAULT_API_URL}/tasks/{task_id}" requests_mock.add( method=responses.DELETE, url=endpoint, status=204, match=[auth_matcher(), request_id_matcher()], ) response = todoist_api.delete_task(task_id) assert len(requests_mock.calls) == 1 assert response is True response = await todoist_api_async.delete_task(task_id) assert len(requests_mock.calls) == 2 assert response is True todoist-api-python-3.1.0/tests/test_authentication.py000066400000000000000000000052331500671641500230700ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from urllib.parse import quote import pytest import responses from tests.data.test_defaults import DEFAULT_OAUTH_URL from tests.utils.test_utils import data_matcher, param_matcher from todoist_api_python._core.endpoints import API_URL # Use new base URL from todoist_api_python.authentication import ( get_auth_token, get_auth_token_async, get_authentication_url, revoke_auth_token, revoke_auth_token_async, ) if TYPE_CHECKING: from todoist_api_python.models import AuthResult def test_get_authentication_url() -> None: client_id = "123" scopes = ["task:add", "data:read", "project:delete"] state = "456" params = ( f"client_id={client_id}&scope={scopes[0]},{scopes[1]},{scopes[2]}&state={state}" ) query = quote(params, safe="=&") expected_url = f"{DEFAULT_OAUTH_URL}/authorize?{query}" url = get_authentication_url(client_id, scopes, state) assert url == expected_url @pytest.mark.asyncio async def test_get_auth_token( requests_mock: responses.RequestsMock, default_auth_response: dict[str, Any], default_auth_result: AuthResult, ) -> None: client_id = "123" client_secret = "456" code = "789" requests_mock.add( responses.POST, f"{DEFAULT_OAUTH_URL}/access_token", json=default_auth_response, status=200, match=[ data_matcher( {"client_id": client_id, "client_secret": client_secret, "code": code} ) ], ) auth_result = get_auth_token(client_id, client_secret, code) assert len(requests_mock.calls) == 1 assert auth_result == default_auth_result auth_result = await get_auth_token_async(client_id, client_secret, code) assert len(requests_mock.calls) == 2 assert auth_result == default_auth_result @pytest.mark.asyncio async def test_revoke_auth_token( requests_mock: responses.RequestsMock, ) -> None: client_id = "123" client_secret = "456" token = "AToken" requests_mock.add( responses.DELETE, f"{API_URL}/access_tokens", match=[ param_matcher( { "client_id": client_id, "client_secret": client_secret, "access_token": token, } ) ], status=200, ) result = revoke_auth_token(client_id, client_secret, token) assert len(requests_mock.calls) == 1 assert result is True result = await revoke_auth_token_async(client_id, client_secret, token) assert len(requests_mock.calls) == 2 assert result is True todoist-api-python-3.1.0/tests/test_http_headers.py000066400000000000000000000013161500671641500225210ustar00rootroot00000000000000from __future__ import annotations from todoist_api_python._core.http_headers import create_headers def test_create_headers_default() -> None: headers = create_headers() assert headers == {} def test_create_headers_authorization() -> None: token = "A Token" headers = create_headers(token=token) assert headers["Authorization"] == f"Bearer {token}" def test_create_headers_content_type() -> None: headers = create_headers(with_content=True) assert headers["Content-Type"] == "application/json; charset=utf-8" def test_create_headers_request_id() -> None: request_id = "12345" headers = create_headers(request_id=request_id) assert headers["X-Request-Id"] == request_id todoist-api-python-3.1.0/tests/test_http_requests.py000066400000000000000000000071551500671641500227700ustar00rootroot00000000000000from __future__ import annotations from typing import Any import pytest import responses from requests import HTTPError, Session from responses.matchers import query_param_matcher from tests.data.test_defaults import DEFAULT_REQUEST_ID, DEFAULT_TOKEN from tests.utils.test_utils import ( auth_matcher, data_matcher, param_matcher, request_id_matcher, ) from todoist_api_python._core.http_requests import delete, get, post EXAMPLE_URL = "https://example.com/" EXAMPLE_PARAMS = {"param1": "value1", "param2": "value2"} EXAMPLE_DATA = {"param3": "value31", "param4": "value4"} EXAMPLE_RESPONSE = {"result": "ok"} @responses.activate def test_get_with_params(default_task_response: dict[str, Any]) -> None: responses.add( method=responses.GET, url=EXAMPLE_URL, json=EXAMPLE_RESPONSE, status=200, match=[ auth_matcher(), request_id_matcher(DEFAULT_REQUEST_ID), param_matcher(EXAMPLE_PARAMS), ], ) response: dict[str, Any] = get( session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN, request_id=DEFAULT_REQUEST_ID, params=EXAMPLE_PARAMS, ) assert len(responses.calls) == 1 assert response == EXAMPLE_RESPONSE @responses.activate def test_get_raise_for_status() -> None: responses.add( method=responses.GET, url=EXAMPLE_URL, json="", status=500, ) with pytest.raises(HTTPError) as error_info: get(Session(), EXAMPLE_URL, DEFAULT_TOKEN) assert error_info.value.response.content == b'""' @responses.activate def test_post_with_data(default_task_response: dict[str, Any]) -> None: responses.add( method=responses.POST, url=EXAMPLE_URL, json=EXAMPLE_RESPONSE, status=200, match=[ auth_matcher(), request_id_matcher(DEFAULT_REQUEST_ID), data_matcher(EXAMPLE_DATA), ], ) response: dict[str, Any] = post( session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN, request_id=DEFAULT_REQUEST_ID, data=EXAMPLE_DATA, ) assert len(responses.calls) == 1 assert response == EXAMPLE_RESPONSE @responses.activate def test_post_return_ok_when_no_response_body() -> None: responses.add( method=responses.POST, url=EXAMPLE_URL, status=204, ) result: bool = post(session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN) assert result is True @responses.activate def test_post_raise_for_status() -> None: responses.add( method=responses.POST, url=EXAMPLE_URL, status=500, ) with pytest.raises(HTTPError): post(session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN) @responses.activate def test_delete_with_params() -> None: responses.add( method=responses.DELETE, url=EXAMPLE_URL, status=204, match=[ auth_matcher(), request_id_matcher(DEFAULT_REQUEST_ID), query_param_matcher(EXAMPLE_PARAMS), ], ) result = delete( session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN, request_id=DEFAULT_REQUEST_ID, params=EXAMPLE_PARAMS, ) assert len(responses.calls) == 1 assert result is True @responses.activate def test_delete_raise_for_status() -> None: responses.add( method=responses.DELETE, url=EXAMPLE_URL, status=500, ) with pytest.raises(HTTPError): delete(session=Session(), url=EXAMPLE_URL, token=DEFAULT_TOKEN) todoist-api-python-3.1.0/tests/test_models.py000066400000000000000000000157141500671641500213410ustar00rootroot00000000000000from __future__ import annotations from tests.data.test_defaults import ( DEFAULT_ATTACHMENT_RESPONSE, DEFAULT_COLLABORATOR_RESPONSE, DEFAULT_COMMENT_RESPONSE, DEFAULT_DUE_RESPONSE, DEFAULT_DURATION_RESPONSE, DEFAULT_LABEL_RESPONSE, DEFAULT_PROJECT_RESPONSE, DEFAULT_PROJECT_RESPONSE_2, DEFAULT_SECTION_RESPONSE, DEFAULT_TASK_RESPONSE, ) from todoist_api_python._core.utils import parse_date, parse_datetime from todoist_api_python.models import ( Attachment, AuthResult, Collaborator, Comment, Due, Duration, Label, Project, Section, Task, ) unexpected_data = {"unexpected_key": "some value"} def test_due_from_dict() -> None: sample_data = dict(DEFAULT_DUE_RESPONSE) sample_data.update(unexpected_data) due = Due.from_dict(sample_data) assert due.date == parse_date(str(sample_data["date"])) assert due.timezone == sample_data["timezone"] assert due.string == sample_data["string"] assert due.lang == sample_data["lang"] assert due.is_recurring == sample_data["is_recurring"] def test_duration_from_dict() -> None: sample_data = dict(DEFAULT_DURATION_RESPONSE) sample_data.update(unexpected_data) duration = Duration.from_dict(sample_data) assert duration.amount == sample_data["amount"] assert duration.unit == sample_data["unit"] def test_project_from_dict() -> None: sample_data = dict(DEFAULT_PROJECT_RESPONSE) sample_data.update(unexpected_data) project = Project.from_dict(sample_data) assert project.id == sample_data["id"] assert project.name == sample_data["name"] assert project.description == sample_data["description"] assert project.parent_id == sample_data["parent_id"] assert project.folder_id == sample_data["folder_id"] assert project.workspace_id == sample_data["workspace_id"] assert project.order == sample_data["child_order"] assert project.color == sample_data["color"] assert project.is_collapsed == sample_data["collapsed"] assert project.is_shared == sample_data["shared"] assert project.is_favorite == sample_data["is_favorite"] assert project.is_inbox_project == sample_data["is_inbox_project"] assert project.can_assign_tasks == sample_data["can_assign_tasks"] assert project.view_style == sample_data["view_style"] assert project.created_at == parse_datetime(str(sample_data["created_at"])) assert project.updated_at == parse_datetime(str(sample_data["updated_at"])) def test_project_url() -> None: inbox = Project.from_dict(dict(DEFAULT_PROJECT_RESPONSE)) assert inbox.url == "https://app.todoist.com/app/inbox" project = Project.from_dict(dict(DEFAULT_PROJECT_RESPONSE_2)) assert project.url == "https://app.todoist.com/app/project/inbox-6X7rfFVPjhvv84XG" def test_task_from_dict() -> None: sample_data = dict(DEFAULT_TASK_RESPONSE) sample_data.update(unexpected_data) task = Task.from_dict(sample_data) assert task.id == sample_data["id"] assert task.content == sample_data["content"] assert task.description == sample_data["description"] assert task.project_id == sample_data["project_id"] assert task.section_id == sample_data["section_id"] assert task.parent_id == sample_data["parent_id"] assert task.labels == sample_data["labels"] assert task.priority == sample_data["priority"] assert task.due == Due.from_dict(sample_data["due"]) assert task.duration == Duration.from_dict(sample_data["duration"]) assert task.is_collapsed == sample_data["collapsed"] assert task.order == sample_data["child_order"] assert task.assignee_id == sample_data["responsible_uid"] assert task.assigner_id == sample_data["assigned_by_uid"] assert task.completed_at == sample_data["completed_at"] assert task.creator_id == sample_data["added_by_uid"] assert task.created_at == parse_datetime(sample_data["added_at"]) assert task.updated_at == parse_datetime(sample_data["updated_at"]) def test_task_url() -> None: task = Task.from_dict(dict(DEFAULT_TASK_RESPONSE)) assert ( task.url == "https://app.todoist.com/app/task/some-task-content-6X7rM8997g3RQmvh" ) def test_section_from_dict() -> None: sample_data = dict(DEFAULT_SECTION_RESPONSE) sample_data.update(unexpected_data) section = Section.from_dict(sample_data) assert section.id == sample_data["id"] assert section.project_id == sample_data["project_id"] assert section.name == sample_data["name"] assert section.order == sample_data["order"] def test_collaborator_from_dict() -> None: sample_data = dict(DEFAULT_COLLABORATOR_RESPONSE) sample_data.update(unexpected_data) collaborator = Collaborator.from_dict(sample_data) assert collaborator.id == sample_data["id"] assert collaborator.email == sample_data["email"] assert collaborator.name == sample_data["name"] def test_attachment_from_dict() -> None: sample_data = dict(DEFAULT_ATTACHMENT_RESPONSE) sample_data.update(unexpected_data) attachment = Attachment.from_dict(sample_data) assert attachment.resource_type == sample_data["resource_type"] assert attachment.file_name == sample_data["file_name"] assert attachment.file_size == sample_data["file_size"] assert attachment.file_type == sample_data["file_type"] assert attachment.file_url == sample_data["file_url"] assert attachment.upload_state == sample_data["upload_state"] assert attachment.image == sample_data["image"] assert attachment.image_width == sample_data["image_width"] assert attachment.image_height == sample_data["image_height"] assert attachment.url == sample_data["url"] assert attachment.title == sample_data["title"] def test_comment_from_dict() -> None: sample_data = dict(DEFAULT_COMMENT_RESPONSE) sample_data.update(unexpected_data) comment = Comment.from_dict(sample_data) assert comment.id == sample_data["id"] assert comment.content == sample_data["content"] assert comment.poster_id == sample_data["posted_uid"] assert comment.posted_at == parse_datetime(sample_data["posted_at"]) assert comment.task_id == sample_data["task_id"] assert comment.project_id == sample_data["project_id"] assert comment.attachment == Attachment.from_dict(sample_data["attachment"]) def test_label_from_dict() -> None: sample_data = dict(DEFAULT_LABEL_RESPONSE) sample_data.update(unexpected_data) label = Label.from_dict(sample_data) assert label.id == sample_data["id"] assert label.name == sample_data["name"] assert label.color == sample_data["color"] assert label.order == sample_data["order"] assert label.is_favorite == sample_data["is_favorite"] def test_auth_result_from_dict() -> None: token = "123" state = "456" sample_data = {"access_token": token, "state": state} sample_data.update(unexpected_data) auth_result = AuthResult.from_dict(sample_data) assert auth_result.access_token == token assert auth_result.state == state todoist-api-python-3.1.0/tests/utils/000077500000000000000000000000001500671641500175755ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/utils/__init__.py000066400000000000000000000000001500671641500216740ustar00rootroot00000000000000todoist-api-python-3.1.0/tests/utils/test_utils.py000066400000000000000000000026331500671641500223520ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Any, TypeVar from responses import matchers from tests.data.test_defaults import ( DEFAULT_TOKEN, ) from todoist_api_python.api import TodoistAPI if TYPE_CHECKING: from collections.abc import AsyncIterable, AsyncIterator, Callable RE_UUID = re.compile(r"^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$", re.IGNORECASE) def auth_matcher() -> Callable[..., Any]: return matchers.header_matcher({"Authorization": f"Bearer {DEFAULT_TOKEN}"}) def request_id_matcher(request_id: str | None = None) -> Callable[..., Any]: return matchers.header_matcher({"X-Request-Id": request_id or RE_UUID}) def param_matcher( params: dict[str, str], cursor: str | None = None ) -> Callable[..., Any]: return matchers.query_param_matcher(params | ({"cursor": cursor} if cursor else {})) def data_matcher(data: dict[str, Any]) -> Callable[..., Any]: return matchers.json_params_matcher(data) def get_todoist_api_patch(method: Callable[..., Any] | None) -> str: module = TodoistAPI.__module__ name = TodoistAPI.__qualname__ return f"{module}.{name}.{method.__name__}" if method else f"{module}.{name}" T = TypeVar("T") async def enumerate_async( iterable: AsyncIterable[T], start: int = 0 ) -> AsyncIterator[tuple[int, T]]: index = start async for value in iterable: yield index, value index += 1 todoist-api-python-3.1.0/todoist_api_python/000077500000000000000000000000001500671641500212125ustar00rootroot00000000000000todoist-api-python-3.1.0/todoist_api_python/__init__.py000066400000000000000000000000001500671641500233110ustar00rootroot00000000000000todoist-api-python-3.1.0/todoist_api_python/_core/000077500000000000000000000000001500671641500223015ustar00rootroot00000000000000todoist-api-python-3.1.0/todoist_api_python/_core/__init__.py000066400000000000000000000000001500671641500244000ustar00rootroot00000000000000todoist-api-python-3.1.0/todoist_api_python/_core/endpoints.py000066400000000000000000000056571500671641500246730ustar00rootroot00000000000000from __future__ import annotations import re import unicodedata API_VERSION = "v1" API_URL = f"https://api.todoist.com/api/{API_VERSION}" OAUTH_URL = "https://todoist.com/oauth" PROJECT_URL = "https://app.todoist.com/app/project" INBOX_URL = "https://app.todoist.com/app/inbox" TASK_URL = "https://app.todoist.com/app/task" TASKS_PATH = "tasks" TASKS_FILTER_PATH = "tasks/filter" TASKS_QUICK_ADD_PATH = "tasks/quick" TASKS_COMPLETED_PATH = "tasks/completed" TASKS_COMPLETED_BY_DUE_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_due_date" TASKS_COMPLETED_BY_COMPLETION_DATE_PATH = f"{TASKS_COMPLETED_PATH}/by_completion_date" PROJECTS_PATH = "projects" PROJECT_ARCHIVE_PATH_SUFFIX = "archive" PROJECT_UNARCHIVE_PATH_SUFFIX = "unarchive" COLLABORATORS_PATH = "collaborators" SECTIONS_PATH = "sections" COMMENTS_PATH = "comments" LABELS_PATH = "labels" SHARED_LABELS_PATH = "labels/shared" SHARED_LABELS_RENAME_PATH = f"{SHARED_LABELS_PATH}/rename" SHARED_LABELS_REMOVE_PATH = f"{SHARED_LABELS_PATH}/remove" AUTHORIZE_PATH = "authorize" ACCESS_TOKEN_PATH = "access_token" # noqa: S105 ACCESS_TOKENS_PATH = "access_tokens" def get_oauth_url(relative_path: str) -> str: """ Generate the URL for a given OAuth endpoint. :param relative_path: The relative path of the endpoint. :return: The URL string for the OAuth endpoint. """ return f"{OAUTH_URL}/{relative_path}" def get_api_url(relative_path: str) -> str: """ Generate the URL for a given API endpoint. :param relative_path: The relative path of the endpoint. :return: The URL string for the API endpoint. """ return f"{API_URL}/{relative_path}" def get_task_url(task_id: str, content: str | None = None) -> str: """ Generate the URL for a given task. :param task_id: The ID of the task. :param content: The content of the task. :return: The URL string for the task view. """ slug = _slugify(content) if content is not None else None path = f"{slug}-{task_id}" if content else task_id return f"{TASK_URL}/{path}" def get_project_url(project_id: str, name: str | None = None) -> str: """ Generate the URL for a given project. :param project_id: The ID of the project. :param name: The name of the project. :return: The URL string for the project view. """ slug = _slugify(name) if name is not None else None path = f"{slug}-{project_id}" if name else project_id return f"{PROJECT_URL}/{path}" def _slugify(value: str) -> str: """ Slugify function borrowed from Django. Convert to ASCII. Convert spaces or repeated dashes to single dashes. Remove characters that aren't alphanumerics, underscores, or hyphens. Convert to lowercase. Strip spaces, dashes, and underscores. """ value = ( unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") ) value = re.sub(r"[^\w\s-]", "", value.lower()) return re.sub(r"[-\s]+", "-", value).strip("-_") todoist-api-python-3.1.0/todoist_api_python/_core/http_headers.py000066400000000000000000000011541500671641500253260ustar00rootroot00000000000000from __future__ import annotations CONTENT_TYPE = ("Content-Type", "application/json; charset=utf-8") AUTHORIZATION = ("Authorization", "Bearer %s") X_REQUEST_ID = ("X-Request-Id", "%s") def create_headers( token: str | None = None, with_content: bool = False, request_id: str | None = None, ) -> dict[str, str]: headers: dict[str, str] = {} if token: headers.update([(AUTHORIZATION[0], AUTHORIZATION[1] % token)]) if with_content: headers.update([CONTENT_TYPE]) if request_id: headers.update([(X_REQUEST_ID[0], X_REQUEST_ID[1] % request_id)]) return headers todoist-api-python-3.1.0/todoist_api_python/_core/http_requests.py000066400000000000000000000042471500671641500255740ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any, TypeVar, cast from requests.status_codes import codes from todoist_api_python._core.http_headers import create_headers if TYPE_CHECKING: from requests import Session # Timeouts for requests. # # 10 seconds for connecting is a recurring default and adheres to python-requests's # recommendation of picking a value slightly larger than a multiple of 3. # # 60 seconds for reading aligns with Todoist's own internal timeout. All requests are # forcefully terminated after this time, so there is no point waiting any longer. TIMEOUT = (10, 60) T = TypeVar("T") def get( session: Session, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, ) -> T: # type: ignore[type-var] headers = create_headers(token=token, request_id=request_id) response = session.get( url, params=params, headers=headers, timeout=TIMEOUT, ) if response.status_code == codes.OK: return cast("T", response.json()) response.raise_for_status() return cast("T", response.ok) def post( session: Session, url: str, token: str | None = None, request_id: str | None = None, *, params: dict[str, Any] | None = None, data: dict[str, Any] | None = None, ) -> T: # type: ignore[type-var] headers = create_headers( token=token, with_content=bool(data), request_id=request_id ) response = session.post( url, headers=headers, data=json.dumps(data) if data else None, params=params, timeout=TIMEOUT, ) if response.status_code == codes.OK: return cast("T", response.json()) response.raise_for_status() return cast("T", response.ok) def delete( session: Session, url: str, token: str | None = None, request_id: str | None = None, params: dict[str, Any] | None = None, ) -> bool: headers = create_headers(token=token, request_id=request_id) response = session.delete(url, params=params, headers=headers, timeout=TIMEOUT) response.raise_for_status() return response.ok todoist-api-python-3.1.0/todoist_api_python/_core/utils.py000066400000000000000000000037141500671641500240200ustar00rootroot00000000000000from __future__ import annotations import asyncio import sys import uuid from datetime import date, datetime, timezone from typing import TYPE_CHECKING, TypeVar, cast if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Iterator if sys.version_info >= (3, 11): from datetime import UTC else: UTC = timezone.utc T = TypeVar("T") async def run_async(func: Callable[[], T]) -> T: loop = asyncio.get_event_loop() return await loop.run_in_executor(None, func) async def generate_async(iterator: Iterator[T]) -> AsyncGenerator[T]: def get_next_item() -> tuple[bool, T | None]: try: return True, next(iterator) except StopIteration: return False, None while True: has_more, item = await run_async(get_next_item) if has_more is True: yield cast("T", item) else: break def format_date(d: date) -> str: """Format a date object as YYYY-MM-DD.""" return d.isoformat() def format_datetime(dt: datetime) -> str: """ Format a datetime object. YYYY-MM-DDTHH:MM:SS for naive datetimes; YYYY-MM-DDTHH:MM:SSZ for aware datetimes. """ if dt.tzinfo is None: return dt.isoformat() return dt.astimezone(UTC).isoformat().replace("+00:00", "Z") def parse_date(date_str: str) -> date: """Parse a YYYY-MM-DD string into a date object.""" return date.fromisoformat(date_str) def parse_datetime(datetime_str: str) -> datetime: """ Parse a string into a datetime object. YYYY-MM-DDTHH:MM:SS for naive datetimes; YYYY-MM-DDTHH:MM:SSZ for aware datetimes. """ from datetime import datetime if datetime_str.endswith("Z"): datetime_str = datetime_str[:-1] + "+00:00" return datetime.fromisoformat(datetime_str) return datetime.fromisoformat(datetime_str) def default_request_id_fn() -> str: """Generate random UUIDv4s as the default request ID.""" return str(uuid.uuid4()) todoist-api-python-3.1.0/todoist_api_python/api.py000066400000000000000000001542571500671641500223530ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Callable, Iterator from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeVar from weakref import finalize import requests from annotated_types import Ge, Le, MaxLen, MinLen, Predicate from todoist_api_python._core.endpoints import ( COLLABORATORS_PATH, COMMENTS_PATH, LABELS_PATH, PROJECT_ARCHIVE_PATH_SUFFIX, PROJECT_UNARCHIVE_PATH_SUFFIX, PROJECTS_PATH, SECTIONS_PATH, SHARED_LABELS_PATH, SHARED_LABELS_REMOVE_PATH, SHARED_LABELS_RENAME_PATH, TASKS_COMPLETED_BY_COMPLETION_DATE_PATH, TASKS_COMPLETED_BY_DUE_DATE_PATH, TASKS_FILTER_PATH, TASKS_PATH, TASKS_QUICK_ADD_PATH, get_api_url, ) from todoist_api_python._core.http_requests import delete, get, post from todoist_api_python._core.utils import ( default_request_id_fn, format_date, format_datetime, ) from todoist_api_python.models import ( Attachment, Collaborator, Comment, Label, Project, Section, Task, ) if TYPE_CHECKING: from datetime import date, datetime from types import TracebackType if sys.version_info >= (3, 11): from typing import Self else: Self = TypeVar("Self", bound="TodoistAPI") LanguageCode = Annotated[str, Predicate(lambda x: len(x) == 2)] # noqa: PLR2004 ColorString = Annotated[ str, Predicate( lambda x: x in ( "berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe", ) ), ] ViewStyle = Annotated[str, Predicate(lambda x: x in ("list", "board", "calendar"))] class TodoistAPI: """ Client for the Todoist API. Provides methods for interacting with Todoist resources like tasks, projects, labels, comments, etc. Manages an HTTP session and handles authentication. Can be used as a context manager to ensure the session is closed properly. """ def __init__( self, token: str, request_id_fn: Callable[[], str] | None = default_request_id_fn, session: requests.Session | None = None, ) -> None: """ Initialize the TodoistAPI client. :param token: Authentication token for the Todoist API. :param request_id_fn: Generator of request IDs for the `X-Request-ID` header. :param session: An optional pre-configured requests `Session` object. """ self._token = token self._request_id_fn = request_id_fn self._session = session or requests.Session() self._finalizer = finalize(self, self._session.close) def __enter__(self) -> Self: """ Enters the runtime context related to this object. The with statement will bind this method's return value to the target(s) specified in the as clause of the statement, if any. :return: This TodoistAPI instance. """ return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: """Exit the runtime context and closes the underlying requests session.""" self._finalizer() def get_task(self, task_id: str) -> Task: """ Get a specific task by its ID. :param task_id: The ID of the task to retrieve. :return: The requested task. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") task_data: dict[str, Any] = get( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Task.from_dict(task_data) def get_tasks( self, *, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, label: str | None = None, ids: list[str] | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Task]]: """ Get an iterable of lists of active tasks. The response is an iterable of lists of active tasks matching the criteria. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param project_id: Filter tasks by project ID. :param section_id: Filter tasks by section ID. :param parent_id: Filter tasks by parent task ID. :param label: Filter tasks by label name. :param ids: A list of the IDs of the tasks to retrieve. :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_PATH) params: dict[str, Any] = {} if project_id is not None: params["project_id"] = project_id if section_id is not None: params["section_id"] = section_id if parent_id is not None: params["parent_id"] = parent_id if label is not None: params["label"] = label if ids is not None: params["ids"] = ",".join(str(i) for i in ids) if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Task.from_dict, self._token, self._request_id_fn, params, ) def filter_tasks( self, *, query: Annotated[str, MaxLen(1024)] | None = None, lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Task]]: """ Get an iterable of lists of active tasks matching the filter. The response is an iterable of lists of active tasks matching the criteria. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param query: Query tasks using Todoist's filter language. :param lang: Language for task content (e.g., 'en'). :param limit: Maximum number of tasks per page. :return: An iterable of lists of tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_FILTER_PATH) params: dict[str, Any] = {} if query is not None: params["query"] = query if lang is not None: params["lang"] = lang if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Task.from_dict, self._token, self._request_id_fn, params, ) def add_task( # noqa: PLR0912 self, content: Annotated[str, MinLen(1), MaxLen(500)], *, description: Annotated[str, MaxLen(16383)] | None = None, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, labels: list[Annotated[str, MaxLen(100)]] | None = None, priority: Annotated[int, Ge(1), Le(4)] | None = None, due_string: Annotated[str, MaxLen(150)] | None = None, due_lang: LanguageCode | None = None, due_date: date | None = None, due_datetime: datetime | None = None, assignee_id: str | None = None, order: int | None = None, auto_reminder: bool | None = None, auto_parse_labels: bool | None = None, duration: Annotated[int, Ge(1)] | None = None, duration_unit: Literal["minute", "day"] | None = None, deadline_date: date | None = None, deadline_lang: LanguageCode | None = None, ) -> Task: """ Create a new task. :param content: The text content of the task. :param project_id: The ID of the project to add the task to. :param section_id: The ID of the section to add the task to. :param parent_id: The ID of the parent task. :param labels: The task's labels (a list of names). :param priority: The priority of the task (4 for very urgent). :param due_string: The due date in natural language format. :param due_lang: Language for parsing the due date (e.g., 'en'). :param due_date: The due date as a date object. :param due_datetime: The due date and time as a datetime object. :param assignee_id: User ID to whom the task is assigned. :param description: Description for the task. :param order: The order of task in the project or section. :param auto_reminder: Whether to add default reminder if date with time is set. :param auto_parse_labels: Whether to parse labels from task content. :param duration: The amount of time the task will take. :param duration_unit: The unit of time for duration. :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: The newly created task. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ endpoint = get_api_url(TASKS_PATH) data: dict[str, Any] = {"content": content} if description is not None: data["description"] = description if project_id is not None: data["project_id"] = project_id if section_id is not None: data["section_id"] = section_id if parent_id is not None: data["parent_id"] = parent_id if labels is not None: data["labels"] = labels if priority is not None: data["priority"] = priority if due_string is not None: data["due_string"] = due_string if due_lang is not None: data["due_lang"] = due_lang if due_date is not None: data["due_date"] = format_date(due_date) if due_datetime is not None: data["due_datetime"] = format_datetime(due_datetime) if assignee_id is not None: data["assignee_id"] = assignee_id if order is not None: data["order"] = order if auto_reminder is not None: data["auto_reminder"] = auto_reminder if auto_parse_labels is not None: data["auto_parse_labels"] = auto_parse_labels if duration is not None: data["duration"] = duration if duration_unit is not None: data["duration_unit"] = duration_unit if deadline_date is not None: data["deadline_date"] = format_date(deadline_date) if deadline_lang is not None: data["deadline_lang"] = deadline_lang task_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Task.from_dict(task_data) def add_task_quick( self, text: str, *, note: str | None = None, reminder: str | None = None, auto_reminder: bool = True, ) -> Task: """ Create a new task using Todoist's Quick Add syntax. This automatically parses dates, deadlines, projects, labels, priorities, etc, from the provided text (e.g., "Buy milk #Shopping @groceries tomorrow p1"). :param text: The task text using Quick Add syntax. :param note: Optional note to be added to the task. :param reminder: Optional reminder date in free form text. :param auto_reminder: Whether to add default reminder if date with time is set. :return: A result object containing the parsed task data and metadata. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response cannot be parsed into a QuickAddResult. """ endpoint = get_api_url(TASKS_QUICK_ADD_PATH) data = { "meta": True, "text": text, "auto_reminder": auto_reminder, } if note is not None: data["note"] = note if reminder is not None: data["reminder"] = reminder task_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Task.from_dict(task_data) def update_task( # noqa: PLR0912 self, task_id: str, *, content: Annotated[str, MinLen(1), MaxLen(500)] | None = None, description: Annotated[str, MaxLen(16383)] | None = None, labels: list[Annotated[str, MaxLen(60)]] | None = None, priority: Annotated[int, Ge(1), Le(4)] | None = None, due_string: Annotated[str, MaxLen(150)] | None = None, due_lang: LanguageCode | None = None, due_date: date | None = None, due_datetime: datetime | None = None, assignee_id: str | None = None, day_order: int | None = None, collapsed: bool | None = None, duration: Annotated[int, Ge(1)] | None = None, duration_unit: Literal["minute", "day"] | None = None, deadline_date: date | None = None, deadline_lang: LanguageCode | None = None, ) -> Task: """ Update an existing task. Only the fields to be updated need to be provided. :param task_id: The ID of the task to update. :param content: The text content of the task. :param description: Description for the task. :param labels: The task's labels (a list of names). :param priority: The priority of the task (4 for very urgent). :param due_string: The due date in natural language format. :param due_lang: Language for parsing the due date (e.g., 'en'). :param due_date: The due date as a date object. :param due_datetime: The due date and time as a datetime object. :param assignee_id: User ID to whom the task is assigned. :param day_order: The order of the task inside Today or Next 7 days view. :param collapsed: Whether the task's sub-tasks are collapsed. :param duration: The amount of time the task will take. :param duration_unit: The unit of time for duration. :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: the updated Task. :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") data: dict[str, Any] = {} if content is not None: data["content"] = content if description is not None: data["description"] = description if labels is not None: data["labels"] = labels if priority is not None: data["priority"] = priority if due_string is not None: data["due_string"] = due_string if due_lang is not None: data["due_lang"] = due_lang if due_date is not None: data["due_date"] = format_date(due_date) if due_datetime is not None: data["due_datetime"] = format_datetime(due_datetime) if assignee_id is not None: data["assignee_id"] = assignee_id if day_order is not None: data["day_order"] = day_order if collapsed is not None: data["collapsed"] = collapsed if duration is not None: data["duration"] = duration if duration_unit is not None: data["duration_unit"] = duration_unit if deadline_date is not None: data["deadline_date"] = format_date(deadline_date) if deadline_lang is not None: data["deadline_lang"] = deadline_lang task_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Task.from_dict(task_data) def complete_task(self, task_id: str) -> bool: """ Complete a task. For recurring tasks, this schedules the next occurrence. For non-recurring tasks, it marks them as completed. :param task_id: The ID of the task to close. :return: True if the task was closed successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/close") return post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def uncomplete_task(self, task_id: str) -> bool: """ Uncomplete a (completed) task. Any parent tasks or sections will also be uncompleted. :param task_id: The ID of the task to reopen. :return: True if the task was uncompleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen") return post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def move_task( self, task_id: str, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, ) -> bool: """ Move a task to a different project, section, or parent task. `project_id` takes predence, followed by `section_id` (which also updates `project_id`), and then `parent_id` (which also updates `section_id` and `project_id`). :param task_id: The ID of the task to move. :param project_id: The ID of the project to move the task to. :param section_id: The ID of the section to move the task to. :param parent_id: The ID of the parent to move the task to. :return: True if the task was moved successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. :raises ValueError: If neither `project_id`, `section_id`, nor `parent_id` is provided. """ if project_id is None and section_id is None and parent_id is None: raise ValueError( "Either `project_id`, `section_id`, or `parent_id` must be provided." ) data: dict[str, Any] = {} if project_id is not None: data["project_id"] = project_id if section_id is not None: data["section_id"] = section_id if parent_id is not None: data["parent_id"] = parent_id endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/move") return post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) def delete_task(self, task_id: str) -> bool: """ Delete a task. :param task_id: The ID of the task to delete. :return: True if the task was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{TASKS_PATH}/{task_id}") return delete( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def get_completed_tasks_by_due_date( self, *, since: datetime, until: datetime, workspace_id: str | None = None, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, filter_query: str | None = None, filter_lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Task]]: """ Get an iterable of lists of completed tasks within a due date range. Retrieves tasks completed within a specific due date range (up to 6 weeks). Supports filtering by workspace, project, section, parent task, or a query. The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param since: Start of the date range (inclusive). :param until: End of the date range (inclusive). :param workspace_id: Filter by workspace ID. :param project_id: Filter by project ID. :param section_id: Filter by section ID. :param parent_id: Filter by parent task ID. :param filter_query: Filter by a query string. :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_DUE_DATE_PATH) params: dict[str, Any] = { "since": format_datetime(since), "until": format_datetime(until), } if workspace_id is not None: params["workspace_id"] = workspace_id if project_id is not None: params["project_id"] = project_id if section_id is not None: params["section_id"] = section_id if parent_id is not None: params["parent_id"] = parent_id if filter_query is not None: params["filter_query"] = filter_query if filter_lang is not None: params["filter_lang"] = filter_lang if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "items", Task.from_dict, self._token, self._request_id_fn, params, ) def get_completed_tasks_by_completion_date( self, *, since: datetime, until: datetime, workspace_id: str | None = None, filter_query: str | None = None, filter_lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Task]]: """ Get an iterable of lists of completed tasks within a date range. Retrieves tasks completed within a specific date range (up to 3 months). Supports filtering by workspace or a filter query. The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param since: Start of the date range (inclusive). :param until: End of the date range (inclusive). :param workspace_id: Filter by workspace ID. :param filter_query: Filter by a query string. :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(TASKS_COMPLETED_BY_COMPLETION_DATE_PATH) params: dict[str, Any] = { "since": format_datetime(since), "until": format_datetime(until), } if workspace_id is not None: params["workspace_id"] = workspace_id if filter_query is not None: params["filter_query"] = filter_query if filter_lang is not None: params["filter_lang"] = filter_lang if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "items", Task.from_dict, self._token, self._request_id_fn, params, ) def get_project(self, project_id: str) -> Project: """ Get a project by its ID. :param project_id: The ID of the project to retrieve. :return: The requested project. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") project_data: dict[str, Any] = get( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Project.from_dict(project_data) def get_projects( self, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Project]]: """ Get an iterable of lists of active projects. The response is an iterable of lists of active projects. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param limit: Maximum number of projects per page. :return: An iterable of lists of projects. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(PROJECTS_PATH) params: dict[str, Any] = {} if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Project.from_dict, self._token, self._request_id_fn, params, ) def add_project( self, name: Annotated[str, MinLen(1), MaxLen(120)], *, description: Annotated[str, MaxLen(16383)] | None = None, parent_id: str | None = None, color: ColorString | None = None, is_favorite: bool | None = None, view_style: ViewStyle | None = None, ) -> Project: """ Create a new project. :param name: The name of the project. :param description: Description for the project (up to 1024 characters). :param parent_id: The ID of the parent project. Set to null for root projects. :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board', default is 'list'). :return: The newly created project. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url(PROJECTS_PATH) data: dict[str, Any] = {"name": name} if parent_id is not None: data["parent_id"] = parent_id if description is not None: data["description"] = description if color is not None: data["color"] = color if is_favorite is not None: data["is_favorite"] = is_favorite if view_style is not None: data["view_style"] = view_style project_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Project.from_dict(project_data) def update_project( self, project_id: str, *, name: Annotated[str, MinLen(1), MaxLen(120)] | None = None, description: Annotated[str, MaxLen(16383)] | None = None, color: ColorString | None = None, is_favorite: bool | None = None, view_style: ViewStyle | None = None, ) -> Project: """ Update an existing project. Only the fields to be updated need to be provided as keyword arguments. :param project_id: The ID of the project to update. :param name: The name of the project. :param description: Description for the project (up to 1024 characters). :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board'). :return: the updated Project. :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") data: dict[str, Any] = {} if name is not None: data["name"] = name if description is not None: data["description"] = description if color is not None: data["color"] = color if is_favorite is not None: data["is_favorite"] = is_favorite if view_style is not None: data["view_style"] = view_style project_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Project.from_dict(project_data) def archive_project(self, project_id: str) -> Project: """ Archive a project. For personal projects, archives it only for the user. For workspace projects, archives it for all members. :param project_id: The ID of the project to archive. :return: The archived project object. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( f"{PROJECTS_PATH}/{project_id}/{PROJECT_ARCHIVE_PATH_SUFFIX}" ) project_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Project.from_dict(project_data) def unarchive_project(self, project_id: str) -> Project: """ Unarchive a project. Restores a previously archived project. :param project_id: The ID of the project to unarchive. :return: The unarchived project object. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ endpoint = get_api_url( f"{PROJECTS_PATH}/{project_id}/{PROJECT_UNARCHIVE_PATH_SUFFIX}" ) project_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Project.from_dict(project_data) def delete_project(self, project_id: str) -> bool: """ Delete a project. All nested sections and tasks will also be deleted. :param project_id: The ID of the project to delete. :return: True if the project was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}") return delete( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def get_collaborators( self, project_id: str, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Collaborator]]: """ Get an iterable of lists of collaborators in shared projects. The response is an iterable of lists of collaborators in shared projects, Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param project_id: The ID of the project. :param limit: Maximum number of collaborators per page. :return: An iterable of lists of collaborators. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}/{COLLABORATORS_PATH}") params: dict[str, Any] = {} if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Collaborator.from_dict, self._token, self._request_id_fn, params, ) def get_section(self, section_id: str) -> Section: """ Get a specific section by its ID. :param section_id: The ID of the section to retrieve. :return: The requested section. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") section_data: dict[str, Any] = get( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Section.from_dict(section_data) def get_sections( self, project_id: str | None = None, *, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Section]]: """ Get an iterable of lists of active sections. Supports filtering by `project_id` and pagination arguments. The response is an iterable of lists of active sections. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param project_id: Filter sections by project ID. :param limit: Maximum number of sections per page. :return: An iterable of lists of sections. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SECTIONS_PATH) params: dict[str, Any] = {} if project_id is not None: params["project_id"] = project_id if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Section.from_dict, self._token, self._request_id_fn, params, ) def add_section( self, name: Annotated[str, MinLen(1), MaxLen(2048)], project_id: str, *, order: int | None = None, ) -> Section: """ Create a new section within a project. :param name: The name of the section. :param project_id: The ID of the project to add the section to. :param order: The order of the section among all sections in the project. :return: The newly created section. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ endpoint = get_api_url(SECTIONS_PATH) data: dict[str, Any] = {"name": name, "project_id": project_id} if order is not None: data["order"] = order section_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Section.from_dict(section_data) def update_section( self, section_id: str, name: Annotated[str, MinLen(1), MaxLen(2048)], ) -> Section: """ Update an existing section. Currently, only `name` can be updated. :param section_id: The ID of the section to update. :param name: The new name for the section. :return: the updated Section. :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") section_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data={"name": name}, ) return Section.from_dict(section_data) def delete_section(self, section_id: str) -> bool: """ Delete a section. All tasks within the section will also be deleted. :param section_id: The ID of the section to delete. :return: True if the section was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}") return delete( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def get_comment(self, comment_id: str) -> Comment: """ Get a specific comment by its ID. :param comment_id: The ID of the comment to retrieve. :return: The requested comment. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") comment_data: dict[str, Any] = get( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Comment.from_dict(comment_data) def get_comments( self, *, project_id: str | None = None, task_id: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Comment]]: """ Get an iterable of lists of comments for a task or project. Requires either `project_id` or `task_id` to be set. The response is an iterable of lists of comments. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param project_id: The ID of the project to retrieve comments for. :param task_id: The ID of the task to retrieve comments for. :param limit: Maximum number of comments per page. :return: An iterable of lists of comments. :raises ValueError: If neither `project_id` nor `task_id` is provided. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if project_id is None and task_id is None: raise ValueError("Either `project_id` or `task_id` must be provided.") endpoint = get_api_url(COMMENTS_PATH) params: dict[str, Any] = {} if project_id is not None: params["project_id"] = project_id if task_id is not None: params["task_id"] = task_id if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Comment.from_dict, self._token, self._request_id_fn, params, ) def add_comment( self, content: Annotated[str, MaxLen(15000)], *, project_id: str | None = None, task_id: str | None = None, attachment: Attachment | None = None, uids_to_notify: list[str] | None = None, ) -> Comment: """ Create a new comment on a task or project. Requires either `project_id` or `task_id` to be set, and can optionally include an `attachment` object. :param content: The text content of the comment (supports Markdown). :param project_id: The ID of the project to add the comment to. :param task_id: The ID of the task to add the comment to. :param attachment: The attachment object to include with the comment. :param uids_to_notify: A list of user IDs to notify. :return: The newly created comment. :raises ValueError: If neither `project_id` nor `task_id` is provided. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ if project_id is None and task_id is None: raise ValueError("Either `project_id` or `task_id` must be provided.") endpoint = get_api_url(COMMENTS_PATH) data: dict[str, Any] = {"content": content} if project_id is not None: data["project_id"] = project_id if task_id is not None: data["task_id"] = task_id if attachment is not None: data["attachment"] = attachment.to_dict() if uids_to_notify is not None: data["uids_to_notify"] = uids_to_notify comment_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Comment.from_dict(comment_data) def update_comment( self, comment_id: str, content: Annotated[str, MaxLen(15000)] ) -> Comment: """ Update an existing comment. Currently, only `content` can be updated. :param comment_id: The ID of the comment to update. :param content: The new text content for the comment. :return: the updated Comment. :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") comment_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data={"content": content}, ) return Comment.from_dict(comment_data) def delete_comment(self, comment_id: str) -> bool: """ Delete a comment. :param comment_id: The ID of the comment to delete. :return: True if the comment was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}") return delete( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def get_label(self, label_id: str) -> Label: """ Get a specific personal label by its ID. :param label_id: The ID of the label to retrieve. :return: The requested label. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") label_data: dict[str, Any] = get( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) return Label.from_dict(label_data) def get_labels( self, *, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[Label]]: """ Get an iterable of lists of personal labels. Supports pagination arguments. The response is an iterable of lists of personal labels. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param limit: ` number of labels per page. :return: An iterable of lists of personal labels. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(LABELS_PATH) params: dict[str, Any] = {} if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", Label.from_dict, self._token, self._request_id_fn, params, ) def add_label( self, name: Annotated[str, MinLen(1), MaxLen(60)], *, color: ColorString | None = None, item_order: int | None = None, is_favorite: bool | None = None, ) -> Label: """ Create a new personal label. :param name: The name of the label. :param color: The color of the label icon. :param item_order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: The newly created label. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ endpoint = get_api_url(LABELS_PATH) data: dict[str, Any] = {"name": name} if color is not None: data["color"] = color if item_order is not None: data["item_order"] = item_order if is_favorite is not None: data["is_favorite"] = is_favorite label_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Label.from_dict(label_data) def update_label( self, label_id: str, *, name: Annotated[str, MinLen(1), MaxLen(60)] | None = None, color: ColorString | None = None, item_order: int | None = None, is_favorite: bool | None = None, ) -> Label: """ Update a personal label. Only the fields to be updated need to be provided as keyword arguments. :param label_id: The ID of the label. :param name: The name of the label. :param color: The color of the label icon. :param item_order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: the updated Label. :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") data: dict[str, Any] = {} if name is not None: data["name"] = name if color is not None: data["color"] = color if item_order is not None: data["item_order"] = item_order if is_favorite is not None: data["is_favorite"] = is_favorite label_data: dict[str, Any] = post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) return Label.from_dict(label_data) def delete_label(self, label_id: str) -> bool: """ Delete a personal label. Instances of the label will be removed from tasks. :param label_id: The ID of the label to delete. :return: True if the label was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(f"{LABELS_PATH}/{label_id}") return delete( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, ) def get_shared_labels( self, *, omit_personal: bool = False, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> Iterator[list[str]]: """ Get an iterable of lists of shared label names. Includes labels from collaborators on shared projects that are not in the user's personal labels. Can optionally exclude personal label names using `omit_personal=True`. Supports pagination arguments. The response is an iterable of lists of shared label names. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param omit_personal: Optional boolean flag to omit personal label names. :param limit: Maximum number of labels per page. :return: An iterable of lists of shared label names (strings). :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ endpoint = get_api_url(SHARED_LABELS_PATH) params: dict[str, Any] = {"omit_personal": omit_personal} if limit is not None: params["limit"] = limit return ResultsPaginator( self._session, endpoint, "results", str, self._token, self._request_id_fn, params, ) def rename_shared_label( self, name: Annotated[str, MaxLen(60)], new_name: Annotated[str, MinLen(1), MaxLen(60)], ) -> bool: """ Rename all occurrences of a shared label across all projects. :param name: The current name of the shared label to rename. :param new_name: The new name for the shared label. :return: True if the rename was successful, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_RENAME_PATH) return post( self._session, endpoint, self._token, params={"name": name}, data={"new_name": new_name}, ) def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool: """ Remove all occurrences of a shared label across all projects. This action removes the label string from all tasks where it appears. :param name: The name of the shared label to remove. :return: True if the removal was successful, :raises requests.exceptions.HTTPError: If the API request fails. """ endpoint = get_api_url(SHARED_LABELS_REMOVE_PATH) data = {"name": name} return post( self._session, endpoint, self._token, self._request_id_fn() if self._request_id_fn else None, data=data, ) T = TypeVar("T") class ResultsPaginator(Iterator[list[T]]): """ Iterator for paginated results from the Todoist API. It encapsulates the logic for fetching and iterating through paginated results from Todoist API endpoints. It handles cursor-based pagination automatically, requesting new pages as needed when iterating. """ _session: requests.Session _url: str _results_field: str _results_inst: Callable[[Any], T] _token: str _cursor: str | None def __init__( self, session: requests.Session, url: str, results_field: str, results_inst: Callable[[Any], T], token: str, request_id_fn: Callable[[], str] | None, params: dict[str, Any], ) -> None: """ Initialize the ResultsPaginator. :param session: The requests Session to use for API calls. :param url: The API endpoint URL to fetch results from. :param results_field: The key in the API response that contains the results. :param results_inst: A callable that converts result items to objects of type T. :param token: The authentication token for the Todoist API. :param params: Query parameters to include in API requests. """ self._session = session self._url = url self._results_field = results_field self._results_inst = results_inst self._token = token self._request_id_fn = request_id_fn self._params = params self._cursor = "" # empty string for first page def __next__(self) -> list[T]: """ Fetch and return the next page of results from the Todoist API. :return: A list of results. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ if self._cursor is None: raise StopIteration params = self._params.copy() if self._cursor != "": params["cursor"] = self._cursor data: dict[str, Any] = get( self._session, self._url, self._token, self._request_id_fn() if self._request_id_fn else None, params, ) self._cursor = data.get("next_cursor") results: list[Any] = data.get(self._results_field, []) return [self._results_inst(result) for result in results] todoist-api-python-3.1.0/todoist_api_python/api_async.py000066400000000000000000001112351500671641500235350ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING, Annotated, Callable, Literal, TypeVar from annotated_types import Ge, Le, MaxLen, MinLen from todoist_api_python._core.utils import ( default_request_id_fn, generate_async, run_async, ) from todoist_api_python.api import TodoistAPI if TYPE_CHECKING: from collections.abc import AsyncGenerator from datetime import date, datetime from types import TracebackType import requests from todoist_api_python.models import ( Attachment, Collaborator, Comment, Label, Project, Section, Task, ) from todoist_api_python.api import ( ColorString, LanguageCode, ViewStyle, ) if sys.version_info >= (3, 11): from typing import Self else: Self = TypeVar("Self", bound="TodoistAPIAsync") class TodoistAPIAsync: """ Async client for the Todoist API. Provides asynchronous methods for interacting with Todoist resources like tasks, projects,labels, comments, etc. Manages an HTTP session and handles authentication. Can be used as an async context manager to ensure the session is closed properly. """ def __init__( self, token: str, request_id_fn: Callable[[], str] | None = default_request_id_fn, session: requests.Session | None = None, ) -> None: """ Initialize the TodoistAPIAsync client. :param token: Authentication token for the Todoist API. :param session: An optional pre-configured requests `Session` object. """ self._api = TodoistAPI(token, request_id_fn, session) async def __aenter__(self) -> Self: """ Enters the async runtime context related to this object. The with statement will bind this method's return value to the target(s) specified in the as clause of the statement, if any. :return: This TodoistAPIAsync instance. """ return self def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: """Exit the async runtime context and closes the underlying requests session.""" async def get_task(self, task_id: str) -> Task: """ Get a specific task by its ID. :param task_id: The ID of the task to retrieve. :return: The requested task. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ return await run_async(lambda: self._api.get_task(task_id)) async def get_tasks( self, *, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, label: str | None = None, ids: list[str] | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Task]]: """ Get a list of active tasks. :param project_id: Filter tasks by project ID. :param section_id: Filter tasks by section ID. :param parent_id: Filter tasks by parent task ID. :param label: Filter tasks by label name. :param ids: A list of the IDs of the tasks to retrieve. :param limit: Maximum number of tasks per page (between 1 and 200). :return: A list of tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_tasks( project_id=project_id, section_id=section_id, parent_id=parent_id, label=label, ids=ids, limit=limit, ) return generate_async(paginator) async def filter_tasks( self, *, query: Annotated[str, MaxLen(1024)] | None = None, lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Task]]: """ Get a lists of active tasks matching the filter. The response is an iterable of lists of active tasks matching the criteria. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param query: Query tasks using Todoist's filter language. :param lang: Language for task content (e.g., 'en'). :param limit: Maximum number of tasks per page (between 1 and 200). :return: An iterable of lists of tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.filter_tasks( query=query, lang=lang, limit=limit, ) return generate_async(paginator) async def add_task( self, content: Annotated[str, MinLen(1), MaxLen(500)], *, description: Annotated[str, MaxLen(16383)] | None = None, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, labels: list[Annotated[str, MaxLen(100)]] | None = None, priority: Annotated[int, Ge(1), Le(4)] | None = None, due_string: Annotated[str, MaxLen(150)] | None = None, due_date: date | None = None, due_datetime: datetime | None = None, due_lang: LanguageCode | None = None, assignee_id: str | None = None, order: int | None = None, auto_reminder: bool | None = None, auto_parse_labels: bool | None = None, duration: Annotated[int, Ge(1)] | None = None, duration_unit: Literal["minute", "day"] | None = None, deadline_date: date | None = None, deadline_lang: LanguageCode | None = None, ) -> Task: """ Create a new task. :param content: The text content of the task. :param project_id: The ID of the project to add the task to. :param section_id: The ID of the section to add the task to. :param parent_id: The ID of the parent task. :param labels: The task's labels (a list of names). :param priority: The priority of the task (4 for very urgent). :param due_string: The due date in natural language format. :param due_lang: Language for parsing the due date (e.g., 'en'). :param due_date: The due date as a date object. :param due_datetime: The due date and time as a datetime object. :param assignee_id: User ID to whom the task is assigned. :param description: Description for the task. :param order: The order of task in the project or section. :param auto_reminder: Whether to add default reminder if date with time is set. :param auto_parse_labels: Whether to parse labels from task content. :param duration: The amount of time the task will take. :param duration_unit: The unit of time for duration. :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: The newly created task. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Task dictionary. """ return await run_async( lambda: self._api.add_task( content, description=description, project_id=project_id, section_id=section_id, parent_id=parent_id, labels=labels, priority=priority, due_string=due_string, due_lang=due_lang, due_date=due_date, due_datetime=due_datetime, assignee_id=assignee_id, order=order, auto_reminder=auto_reminder, auto_parse_labels=auto_parse_labels, duration=duration, duration_unit=duration_unit, deadline_date=deadline_date, deadline_lang=deadline_lang, ) ) async def add_task_quick( self, text: str, *, note: str | None = None, reminder: str | None = None, auto_reminder: bool = True, ) -> Task: """ Create a new task using Todoist's Quick Add syntax. This automatically parses dates, deadlines, projects, labels, priorities, etc, from the provided text (e.g., "Buy milk #Shopping @groceries tomorrow p1"). :param text: The task text using Quick Add syntax. :param note: Optional note to be added to the task. :param reminder: Optional reminder date in free form text. :param auto_reminder: Whether to add default reminder if date with time is set. :return: A result object containing the parsed task data and metadata. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response cannot be parsed into a QuickAddResult. """ return await run_async( lambda: self._api.add_task_quick( text, note=note, reminder=reminder, auto_reminder=auto_reminder ) ) async def update_task( self, task_id: str, *, content: Annotated[str, MinLen(1), MaxLen(500)] | None = None, description: Annotated[str, MaxLen(16383)] | None = None, labels: list[Annotated[str, MaxLen(60)]] | None = None, priority: Annotated[int, Ge(1), Le(4)] | None = None, due_string: Annotated[str, MaxLen(150)] | None = None, due_lang: LanguageCode | None = None, due_date: date | None = None, due_datetime: datetime | None = None, assignee_id: str | None = None, day_order: int | None = None, collapsed: bool | None = None, duration: Annotated[int, Ge(1)] | None = None, duration_unit: Literal["minute", "day"] | None = None, deadline_date: date | None = None, deadline_lang: LanguageCode | None = None, ) -> Task: """ Update an existing task. Only the fields to be updated need to be provided. :param task_id: The ID of the task to update. :param content: The text content of the task. :param description: Description for the task. :param labels: The task's labels (a list of names). :param priority: The priority of the task (4 for very urgent). :param due_string: The due date in natural language format. :param due_lang: Language for parsing the due date (e.g., 'en'). :param due_date: The due date as a date object. :param due_datetime: The due date and time as a datetime object. :param assignee_id: User ID to whom the task is assigned. :param day_order: The order of the task inside Today or Next 7 days view. :param collapsed: Whether the task's sub-tasks are collapsed. :param duration: The amount of time the task will take. :param duration_unit: The unit of time for duration. :param deadline_date: The deadline date as a date object. :param deadline_lang: Language for parsing the deadline date. :return: the updated Task. :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async( lambda: self._api.update_task( task_id, content=content, description=description, labels=labels, priority=priority, due_string=due_string, due_date=due_date, due_datetime=due_datetime, due_lang=due_lang, assignee_id=assignee_id, day_order=day_order, collapsed=collapsed, duration=duration, duration_unit=duration_unit, deadline_date=deadline_date, deadline_lang=deadline_lang, ) ) async def complete_task(self, task_id: str) -> bool: """ Complete a task. For recurring tasks, this schedules the next occurrence. For non-recurring tasks, it marks them as completed. :param task_id: The ID of the task to close. :return: True if the task was closed successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.complete_task(task_id)) async def uncomplete_task(self, task_id: str) -> bool: """ Uncomplete a (completed) task. Any parent tasks or sections will also be uncompleted. :param task_id: The ID of the task to reopen. :return: True if the task was uncompleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.uncomplete_task(task_id)) async def move_task( self, task_id: str, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, ) -> bool: """ Move a task to a different project, section, or parent task. `project_id` takes predence, followed by `section_id` (which also updates `project_id`), and then `parent_id` (which also updates `section_id` and `project_id`). :param task_id: The ID of the task to move. :param project_id: The ID of the project to move the task to. :param section_id: The ID of the section to move the task to. :param parent_id: The ID of the parent to move the task to. :return: True if the task was moved successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. :raises ValueError: If neither `project_id`, `section_id`, nor `parent_id` is provided. """ return await run_async( lambda: self._api.move_task( task_id, project_id=project_id, section_id=section_id, parent_id=parent_id, ) ) async def delete_task(self, task_id: str) -> bool: """ Delete a task. :param task_id: The ID of the task to delete. :return: True if the task was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.delete_task(task_id)) async def get_completed_tasks_by_due_date( self, *, since: datetime, until: datetime, workspace_id: str | None = None, project_id: str | None = None, section_id: str | None = None, parent_id: str | None = None, filter_query: str | None = None, filter_lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Task]]: """ Get an iterable of lists of completed tasks within a due date range. Retrieves tasks completed within a specific due date range (up to 6 weeks). Supports filtering by workspace, project, section, parent task, or a query. The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param since: Start of the date range (inclusive). :param until: End of the date range (inclusive). :param workspace_id: Filter by workspace ID. :param project_id: Filter by project ID. :param section_id: Filter by section ID. :param parent_id: Filter by parent task ID. :param filter_query: Filter by a query string. :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_completed_tasks_by_due_date( since=since, until=until, workspace_id=workspace_id, project_id=project_id, section_id=section_id, parent_id=parent_id, filter_query=filter_query, filter_lang=filter_lang, limit=limit, ) return generate_async(paginator) async def get_completed_tasks_by_completion_date( self, *, since: datetime, until: datetime, workspace_id: str | None = None, filter_query: str | None = None, filter_lang: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Task]]: """ Get an iterable of lists of completed tasks within a date range. Retrieves tasks completed within a specific date range (up to 3 months). Supports filtering by workspace or a filter query. The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions. :param since: Start of the date range (inclusive). :param until: End of the date range (inclusive). :param workspace_id: Filter by workspace ID. :param filter_query: Filter by a query string. :param filter_lang: Language for the filter query (e.g., 'en'). :param limit: Maximum number of tasks per page (default 50). :return: An iterable of lists of completed tasks. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_completed_tasks_by_completion_date( since=since, until=until, workspace_id=workspace_id, filter_query=filter_query, filter_lang=filter_lang, limit=limit, ) return generate_async(paginator) async def get_project(self, project_id: str) -> Project: """ Get a project by its ID. :param project_id: The ID of the project to retrieve. :return: The requested project. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ return await run_async(lambda: self._api.get_project(project_id)) async def get_projects( self, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Project]]: """ Get a list of active projects. :param limit: Maximum number of projects per page. :return: A list of projects. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_projects(limit=limit) return generate_async(paginator) async def add_project( self, name: Annotated[str, MinLen(1), MaxLen(120)], *, description: Annotated[str, MaxLen(16383)] | None = None, parent_id: str | None = None, color: ColorString | None = None, is_favorite: bool | None = None, view_style: ViewStyle | None = None, ) -> Project: """ Create a new project. :param name: The name of the project. :param description: Description for the project (up to 1024 characters). :param parent_id: The ID of the parent project. Set to null for root projects. :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board', default is 'list'). :return: The newly created project. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ return await run_async( lambda: self._api.add_project( name, description=description, parent_id=parent_id, color=color, is_favorite=is_favorite, view_style=view_style, ) ) async def update_project( self, project_id: str, *, name: Annotated[str, MinLen(1), MaxLen(120)] | None = None, description: Annotated[str, MaxLen(16383)] | None = None, color: ColorString | None = None, is_favorite: bool | None = None, view_style: ViewStyle | None = None, ) -> Project: """ Update an existing project. Only the fields to be updated need to be provided as keyword arguments. :param project_id: The ID of the project to update. :param name: The name of the project. :param description: Description for the project (up to 1024 characters). :param color: The color of the project icon. :param is_favorite: Whether the project is a favorite. :param view_style: A string value (either 'list' or 'board'). :return: the updated Project. :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async( lambda: self._api.update_project( project_id, name=name, description=description, color=color, is_favorite=is_favorite, view_style=view_style, ) ) async def archive_project(self, project_id: str) -> Project: """ Archive a project. For personal projects, archives it only for the user. For workspace projects, archives it for all members. :param project_id: The ID of the project to archive. :return: The archived project object. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ return await run_async(lambda: self._api.archive_project(project_id)) async def unarchive_project(self, project_id: str) -> Project: """ Unarchive a project. Restores a previously archived project. :param project_id: The ID of the project to unarchive. :return: The unarchived project object. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Project dictionary. """ return await run_async(lambda: self._api.unarchive_project(project_id)) async def delete_project(self, project_id: str) -> bool: """ Delete a project. All nested sections and tasks will also be deleted. :param project_id: The ID of the project to delete. :return: True if the project was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.delete_project(project_id)) async def get_collaborators( self, project_id: str, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Collaborator]]: """ Get a list of collaborators in shared projects. :param project_id: The ID of the project. :param limit: Maximum number of collaborators per page. :return: A list of collaborators. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_collaborators(project_id, limit=limit) return generate_async(paginator) async def get_section(self, section_id: str) -> Section: """ Get a specific section by its ID. :param section_id: The ID of the section to retrieve. :return: The requested section. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ return await run_async(lambda: self._api.get_section(section_id)) async def get_sections( self, project_id: str | None = None, *, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Section]]: """ Get a list of active sections. Supports filtering by `project_id` and pagination arguments. :param project_id: Filter sections by project ID. :param limit: Maximum number of sections per page (between 1 and 200). :return: A list of sections. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_sections(project_id=project_id, limit=limit) return generate_async(paginator) async def add_section( self, name: Annotated[str, MinLen(1), MaxLen(2048)], project_id: str, *, order: int | None = None, ) -> Section: """ Create a new section within a project. :param name: The name of the section. :param project_id: The ID of the project to add the section to. :param order: The order of the section among all sections in the project. :return: The newly created section. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Section dictionary. """ return await run_async( lambda: self._api.add_section(name, project_id, order=order) ) async def update_section( self, section_id: str, name: Annotated[str, MinLen(1), MaxLen(2048)], ) -> Section: """ Update an existing section. Currently, only `name` can be updated. :param section_id: The ID of the section to update. :param name: The new name for the section. :return: the updated Section. :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.update_section(section_id, name)) async def delete_section(self, section_id: str) -> bool: """ Delete a section. All tasks within the section will also be deleted. :param section_id: The ID of the section to delete. :return: True if the section was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.delete_section(section_id)) async def get_comment(self, comment_id: str) -> Comment: """ Get a specific comment by its ID. :param comment_id: The ID of the comment to retrieve. :return: The requested comment. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ return await run_async(lambda: self._api.get_comment(comment_id)) async def get_comments( self, *, project_id: str | None = None, task_id: str | None = None, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Comment]]: """ Get a list of comments for a task or project. Requires either `project_id` or `task_id` to be set. :param project_id: The ID of the project to retrieve comments for. :param task_id: The ID of the task to retrieve comments for. :param limit: Maximum number of comments per page (between 1 and 200). :return: A list of comments. :raises ValueError: If neither `project_id` nor `task_id` is provided. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_comments( project_id=project_id, task_id=task_id, limit=limit ) return generate_async(paginator) async def add_comment( self, content: Annotated[str, MaxLen(15000)], *, project_id: str | None = None, task_id: str | None = None, attachment: Attachment | None = None, uids_to_notify: list[str] | None = None, ) -> Comment: """ Create a new comment on a task or project. Requires either `project_id` or `task_id` to be set, and can optionally include an `attachment` object. :param content: The text content of the comment (supports Markdown). :param project_id: The ID of the project to add the comment to. :param task_id: The ID of the task to add the comment to. :param attachment: The attachment object to include with the comment. :param uids_to_notify: A list of user IDs to notify. :return: The newly created comment. :raises ValueError: If neither `project_id` nor `task_id` is provided. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Comment dictionary. """ return await run_async( lambda: self._api.add_comment( content, project_id=project_id, task_id=task_id, attachment=attachment, uids_to_notify=uids_to_notify, ) ) async def update_comment( self, comment_id: str, content: Annotated[str, MaxLen(15000)] ) -> Comment: """ Update an existing comment. Currently, only `content` can be updated. :param comment_id: The ID of the comment to update. :param content: The new text content for the comment. :return: the updated Comment. :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.update_comment(comment_id, content)) async def delete_comment(self, comment_id: str) -> bool: """ Delete a comment. :param comment_id: The ID of the comment to delete. :return: True if the comment was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.delete_comment(comment_id)) async def get_label(self, label_id: str) -> Label: """ Get a specific personal label by its ID. :param label_id: The ID of the label to retrieve. :return: The requested label. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ return await run_async(lambda: self._api.get_label(label_id)) async def get_labels( self, *, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[Label]]: """ Get a list of personal labels. Supports pagination arguments. :param limit: Maximum number of labels per page (between 1 and 200). :return: A list of personal labels. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_labels(limit=limit) return generate_async(paginator) async def add_label( self, name: Annotated[str, MinLen(1), MaxLen(60)], *, color: ColorString | None = None, item_order: int | None = None, is_favorite: bool | None = None, ) -> Label: """ Create a new personal label. :param name: The name of the label. :param color: The color of the label icon. :param item_order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: The newly created label. :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response is not a valid Label dictionary. """ return await run_async( lambda: self._api.add_label( name, color=color, item_order=item_order, is_favorite=is_favorite ) ) async def update_label( self, label_id: str, *, name: Annotated[str, MinLen(1), MaxLen(60)] | None = None, color: ColorString | None = None, item_order: int | None = None, is_favorite: bool | None = None, ) -> Label: """ Update a personal label. Only the fields to be updated need to be provided as keyword arguments. :param label_id: The ID of the label. :param name: The name of the label. :param color: The color of the label icon. :param item_order: Label's order in the label list. :param is_favorite: Whether the label is a favorite. :return: the updated Label. :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async( lambda: self._api.update_label( label_id, name=name, color=color, item_order=item_order, is_favorite=is_favorite, ) ) async def delete_label(self, label_id: str) -> bool: """ Delete a personal label. Instances of the label will be removed from tasks. :param label_id: The ID of the label to delete. :return: True if the label was deleted successfully, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.delete_label(label_id)) async def get_shared_labels( self, *, omit_personal: bool = False, limit: Annotated[int, Ge(1), Le(200)] | None = None, ) -> AsyncGenerator[list[str]]: """ Get a list of shared label names. Includes labels from collaborators on shared projects that are not in the user's personal labels. Can optionally exclude personal label names using `omit_personal=True`. Supports pagination arguments. :param omit_personal: Optional boolean flag to omit personal label names. :param limit: Maximum number of labels per page (between 1 and 200). :return: A list of shared label names (strings). :raises requests.exceptions.HTTPError: If the API request fails. :raises TypeError: If the API response structure is unexpected. """ paginator = self._api.get_shared_labels( omit_personal=omit_personal, limit=limit ) return generate_async(paginator) async def rename_shared_label( self, name: Annotated[str, MaxLen(60)], new_name: Annotated[str, MinLen(1), MaxLen(60)], ) -> bool: """ Rename all occurrences of a shared label across all projects. :param name: The current name of the shared label to rename. :param new_name: The new name for the shared label. :return: True if the rename was successful, False otherwise (possibly raise `HTTPError` instead). :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.rename_shared_label(name, new_name)) async def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool: """ Remove all occurrences of a shared label across all projects. This action removes the label string from all tasks where it appears. :param name: The name of the shared label to remove. :return: True if the removal was successful, :raises requests.exceptions.HTTPError: If the API request fails. """ return await run_async(lambda: self._api.remove_shared_label(name)) todoist-api-python-3.1.0/todoist_api_python/authentication.py000066400000000000000000000053141500671641500246060ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Literal from urllib.parse import urlencode import requests from requests import Session from todoist_api_python._core.endpoints import ( ACCESS_TOKEN_PATH, ACCESS_TOKENS_PATH, AUTHORIZE_PATH, get_api_url, get_oauth_url, ) from todoist_api_python._core.http_requests import delete, post from todoist_api_python._core.utils import run_async from todoist_api_python.models import AuthResult """ Possible permission scopes: - `data:read`: Read-only access - `data:read_write`: Read and write access - `data:delete`: Full access including delete - `task:add`: Can create new tasks - `project:delete`: Can delete projects - `backups:read`: Can access user backups without MFA """ Scope = Literal[ "task:add", "data:read", "data:read_write", "data:delete", "project:delete", "backups:read", ] def get_authentication_url(client_id: str, scopes: list[Scope], state: str) -> str: """Get authorization URL to initiate OAuth flow.""" if len(scopes) == 0: raise ValueError("At least one authorization scope should be requested.") endpoint = get_oauth_url(AUTHORIZE_PATH) query = { "client_id": client_id, "scope": ",".join(scopes), "state": state, } return f"{endpoint}?{urlencode(query)}" def get_auth_token( client_id: str, client_secret: str, code: str, session: Session | None = None ) -> AuthResult: """Get access token using provided client ID, client secret, and auth code.""" endpoint = get_oauth_url(ACCESS_TOKEN_PATH) session = session or requests.Session() data = { "client_id": client_id, "client_secret": client_secret, "code": code, } response: dict[str, Any] = post(session=session, url=endpoint, data=data) return AuthResult.from_dict(response) async def get_auth_token_async( client_id: str, client_secret: str, code: str ) -> AuthResult: return await run_async(lambda: get_auth_token(client_id, client_secret, code)) def revoke_auth_token( client_id: str, client_secret: str, token: str, session: Session | None = None ) -> bool: """Revoke an access token.""" # `get_api_url` is not a typo. Deleting access tokens is done using the regular API. endpoint = get_api_url(ACCESS_TOKENS_PATH) session = session or requests.Session() params = { "client_id": client_id, "client_secret": client_secret, "access_token": token, } return delete(session=session, url=endpoint, params=params) async def revoke_auth_token_async( client_id: str, client_secret: str, token: str ) -> bool: return await run_async(lambda: revoke_auth_token(client_id, client_secret, token)) todoist-api-python-3.1.0/todoist_api_python/models.py000066400000000000000000000130221500671641500230450ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Annotated, Literal, Optional, Union from dataclass_wizard import JSONPyWizard from dataclass_wizard.v1 import DatePattern, DateTimePattern, UTCDateTimePattern from dataclass_wizard.v1.models import Alias from todoist_api_python._core.endpoints import INBOX_URL, get_project_url, get_task_url ViewStyle = Literal["list", "board", "calendar"] DurationUnit = Literal["minute", "day"] ApiDate = UTCDateTimePattern["%FT%T.%fZ"] # type: ignore[valid-type] ApiDue = Union[ # https://github.com/rnag/dataclass-wizard/issues/189 DatePattern["%F"], DateTimePattern["%FT%T"], UTCDateTimePattern["%FT%TZ"] # type: ignore[valid-type] # noqa: F722 ] @dataclass class Project(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str name: str description: str order: Annotated[int, Alias(load=("child_order", "order"))] color: str is_collapsed: Annotated[bool, Alias(load=("collapsed", "is_collapsed"))] is_shared: Annotated[bool, Alias(load=("shared", "is_shared"))] is_favorite: bool is_archived: bool can_assign_tasks: bool view_style: ViewStyle created_at: ApiDate updated_at: ApiDate parent_id: str | None = None is_inbox_project: Annotated[ bool | None, Alias(load=("inbox_project", "is_inbox_project")) ] = None workspace_id: str | None = None folder_id: str | None = None @property def url(self) -> str: if self.is_inbox_project: return INBOX_URL return get_project_url(self.id, self.name) @dataclass class Section(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str name: str project_id: str is_collapsed: Annotated[bool, Alias(load=("collapsed", "is_collapsed"))] order: Annotated[int, Alias(load=("section_order", "order"))] @dataclass class Due(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True date: ApiDue string: str lang: str = "en" is_recurring: bool = False timezone: str | None = None @dataclass class Deadline(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True date: ApiDue lang: str = "en" @dataclass class Meta(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True project: tuple[str, str] section: tuple[str, str] assignee: tuple[str, str] labels: dict[int, str] due: Due | None deadline: Deadline | None @dataclass class Task(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str content: str description: str project_id: str section_id: str | None parent_id: str | None labels: list[str] | None priority: int due: Due | None deadline: Deadline | None duration: Duration | None is_collapsed: Annotated[bool, Alias(load=("collapsed", "is_collapsed"))] order: Annotated[int, Alias(load=("child_order", "order"))] assignee_id: Annotated[str | None, Alias(load=("responsible_uid", "assignee_id"))] assigner_id: Annotated[str | None, Alias(load=("assigned_by_uid", "assigner_id"))] completed_at: Optional[ApiDate] # noqa: UP007 # https://github.com/rnag/dataclass-wizard/issues/189 creator_id: Annotated[str, Alias(load=("added_by_uid", "creator_id"))] created_at: Annotated[ApiDate, Alias(load=("added_at", "created_at"))] updated_at: ApiDate meta: Meta | None = None @property def url(self) -> str: return get_task_url(self.id, self.content) @property def is_completed(self) -> bool: return self.completed_at is not None @dataclass class Collaborator(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str email: str name: str @dataclass class Attachment(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True resource_type: str | None = None file_name: str | None = None file_size: int | None = None file_type: str | None = None file_url: str | None = None file_duration: int | None = None upload_state: str | None = None image: str | None = None image_width: int | None = None image_height: int | None = None url: str | None = None title: str | None = None @dataclass class Comment(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str content: str poster_id: Annotated[str, Alias(load=("posted_uid", "poster_id"))] posted_at: ApiDate task_id: Annotated[str | None, Alias(load=("item_id", "task_id"))] = None project_id: str | None = None attachment: Annotated[ Attachment | None, Alias(load=("file_attachment", "attachment")) ] = None def __post_init__(self) -> None: """ Finish initialization of the Comment object. :raises ValueError: If neither `task_id` nor `project_id` is specified. """ if self.task_id is None and self.project_id is None: raise ValueError("Must specify `task_id` or `project_id`") @dataclass class Label(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str name: str color: str order: int is_favorite: bool @dataclass class AuthResult(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True access_token: str state: str | None @dataclass class Duration(JSONPyWizard): class _(JSONPyWizard.Meta): # noqa:N801 v1 = True amount: int unit: DurationUnit todoist-api-python-3.1.0/todoist_api_python/py.typed000066400000000000000000000000001500671641500226770ustar00rootroot00000000000000todoist-api-python-3.1.0/tox.ini000066400000000000000000000002071500671641500166050ustar00rootroot00000000000000[tox] envlist = py39,py310,py311,py312,py313 [testenv] runner = uv-venv-lock-runner deps = pytest commands = pytest {posargs} todoist-api-python-3.1.0/uv.lock000066400000000000000000003656271500671641500166220ustar00rootroot00000000000000version = 1 revision = 1 requires-python = ">=3.9, <4" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] [[package]] name = "backrefs" version = "5.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, ] [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] [[package]] name = "chardet" version = "5.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "dataclass-wizard" version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/c5/10f2bd575b4fee1cf26b0ffa5fead2d6525f950a797566437a51fa08a94f/dataclass-wizard-0.35.0.tar.gz", hash = "sha256:8e4b254991bf93416a48e2911bb985e3787cff11f00270c3d1165d2523cb3fb6", size = 295578 } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/95/968dff23cfd82806bf0e2d1c155227717046aed5e7afd8bc0fefdc7eaaf3/dataclass_wizard-0.35.0-py2.py3-none-any.whl", hash = "sha256:3bb19292477f0bebb12e9cc9178f1a6b93d133af4ae065abf14b713142b32edf", size = 176558 }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, ] [[package]] name = "griffe" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, ] [[package]] name = "identify" version = "2.6.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } wheels = [ { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "importlib-metadata" version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } wheels = [ { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, ] [[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 } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "markdown" version = "3.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } wheels = [ { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355 } wheels = [ { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047 }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, ] [[package]] name = "mkdocs-material" version = "9.6.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } wheels = [ { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, ] [[package]] name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } wheels = [ { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, ] [package.optional-dependencies] python = [ { name = "mkdocstrings-python" }, ] [[package]] name = "mkdocstrings-python" version = "1.16.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } wheels = [ { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, ] [[package]] name = "mypy" version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } wheels = [ { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129 }, { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335 }, { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935 }, { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827 }, { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924 }, { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176 }, { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[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 } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] name = "platformdirs" version = "4.3.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pre-commit" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pymdown-extensions" version = "10.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846 } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467 }, ] [[package]] name = "pyproject-api" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714 } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131 }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "pytest-asyncio" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } wheels = [ { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] [[package]] name = "pyyaml-env-tag" version = "0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, ] [[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 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] name = "responses" version = "0.25.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/7e/2345ac3299bd62bd7163216702bbc88976c099cfceba5b889f2a457727a1/responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb", size = 79203 } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/fc/1d20b64fa90e81e4fa0a34c9b0240a6cfb1326b7e06d18a5432a9917c316/responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c", size = 34732 }, ] [[package]] name = "ruff" version = "0.11.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } wheels = [ { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] name = "todoist-api-python" version = "3.1.0" source = { editable = "." } dependencies = [ { name = "annotated-types" }, { name = "dataclass-wizard" }, { name = "requests" }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "responses" }, { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, { name = "types-requests" }, ] docs = [ { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, ] [package.metadata] requires-dist = [ { name = "annotated-types" }, { name = "dataclass-wizard", specifier = ">=0.35.0,<1.0" }, { name = "requests", specifier = ">=2.32.3,<3" }, ] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = "~=1.11" }, { name = "pre-commit", specifier = ">=4.0.0,<5" }, { name = "pytest", specifier = ">=8.0.0,<9" }, { name = "pytest-asyncio", specifier = ">=0.26.0,<0.27" }, { name = "responses", specifier = ">=0.25.3,<0.26" }, { name = "ruff", specifier = ">=0.11.0,<0.12" }, { name = "tox", specifier = ">=4.15.1,<5" }, { name = "tox-uv", specifier = ">=1.25.0,<2" }, { name = "types-requests", specifier = "~=2.32" }, ] docs = [ { name = "mkdocs", specifier = ">=1.6.1,<2.0.0" }, { name = "mkdocs-material", specifier = ">=9.6.11,<10.0.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.1,<1.0.0" }, ] [[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 } 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tox" version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "chardet" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/87/692478f0a194f1cad64803692642bd88c12c5b64eee16bf178e4a32e979c/tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52", size = 196255 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/38/33348de6fc4b1afb3d76d8485c8aecbdabcfb3af8da53d40c792332e2b37/tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c", size = 172420 }, ] [[package]] name = "tox-uv" version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/3a/3e445f25978a716ba6674f33f687d9336d0312086a277a778a5e9e9220d7/tox_uv-1.25.0.tar.gz", hash = "sha256:59ee5e694c41fef7bbcf058f22a5f9b6a8509698def2ea60c08554f4e36b9fcc", size = 21114 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a7/f5c29e0e6faaccefcab607f672b176927144e9412c8183d21301ea2a6f6c/tox_uv-1.25.0-py3-none-any.whl", hash = "sha256:50cfe7795dcd49b2160d7d65b5ece8717f38cfedc242c852a40ec0a71e159bf7", size = 16431 }, ] [[package]] name = "types-requests" version = "2.32.0.20250328" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 }, ] [[package]] name = "typing-extensions" version = "4.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] [[package]] name = "uv" version = "0.6.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/28/ba/1a5e6dcaa5412081fc900f44403f61188c035565e7df5bf658c266c90539/uv-0.6.16.tar.gz", hash = "sha256:965312f4fd9dda88f688e23edad34324abd1e094acfc813bb476f8bf9a18e44b", size = 3269694 } wheels = [ { url = "https://files.pythonhosted.org/packages/33/ec/277eda61ccd12db9707b8671e5cc5894a88b08c17051d7ae8314867c8c18/uv-0.6.16-py3-none-linux_armv6l.whl", hash = "sha256:e5bba128f384b89ffeb9625e6f753ef1612f900366b8aa48e0e5a44747a69121", size = 16506806 }, { url = "https://files.pythonhosted.org/packages/a8/1a/a45138b79f4f398546a14a3103f0be13e0d4ab742dc7aee21d8f2c5eee86/uv-0.6.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29c5833ee02d92858e711d6403934e0118adc998aadc50b714c3b9ec06561351", size = 16605320 }, { url = "https://files.pythonhosted.org/packages/5a/cb/1dbd857137f9ecffad30f0c2349dfa21d9f54f2677c2f484770942578b68/uv-0.6.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64eb34dcb72fc4b97c634f6b0efea82efe0132ecb47aaebdda29d20befe40b83", size = 15301092 }, { url = "https://files.pythonhosted.org/packages/86/1b/a6eaf596a88ba7e512c4139320ad4859fb53225576f5959f90039b78692d/uv-0.6.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:eb9a6af2351ddeae6fb4e527df9c46323f2a3ff6b005b404b57b32bf41f0a451", size = 15718449 }, { url = "https://files.pythonhosted.org/packages/cd/d1/3f5da1df02ca15d48933875be14d7f72d0e968a0b3de454da15ba36b550a/uv-0.6.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:783051db6b6ff9b37664e85469903894879c2b9ca3a6ee99ad43e2e670607cae", size = 16229773 }, { url = "https://files.pythonhosted.org/packages/bc/d3/92170337bce936c9e8368065d3e3ec570fc1e21456285c6ca8a6fcfc2412/uv-0.6.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61f7cf29224eae670c7a52316fdaa991ecc6bb03ecd15dea94127f324b72a349", size = 16863131 }, { url = "https://files.pythonhosted.org/packages/49/a7/5c0523c6cfd239ff1b61fc8898278c3a0e6923bb77f371d9a0056fea99d9/uv-0.6.16-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:61a143ee717017fa613d5932c4498d6a53730f2259c93ee1138d97e138742cfc", size = 17795899 }, { url = "https://files.pythonhosted.org/packages/b9/24/af283239485b66360528fff68559dbdba4040d47cd7e5c297d629ed3077c/uv-0.6.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:709d35b8f6218fff54be1c7be72ef03829012b9499e57e5235dcbfb726cc8f60", size = 17537650 }, { url = "https://files.pythonhosted.org/packages/22/0b/d9124e59a6d5ba1fdc878be9b17e9372d1dc55de2f2a64762b5e62980dce/uv-0.6.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ba02ea37b974d349ab7aaebd19cd0f11bf3d43db3267460eec511d2e40d0ef5", size = 21798464 }, { url = "https://files.pythonhosted.org/packages/ef/8f/5ad211baa88ecd3ae1a4c17af987f6ae7106cc3020d5bf2ede317902482f/uv-0.6.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e81c8cc7f2f23afb35860a6acd246e2d4bd28da18c259bf82e11f9157675d2a", size = 17258643 }, { url = "https://files.pythonhosted.org/packages/66/dd/f94bf87c703001ece8dea163c3e270401971102ec6c18f735249f4b126c3/uv-0.6.16-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d5a179f2f52ada41dc4390053d61697bb446eadba4db5c5ce99907b65e866886", size = 15991197 }, { url = "https://files.pythonhosted.org/packages/ac/fc/fb766b778ea1ac1f5b10754d1916570a8abbbf95a975f6c1792fc90a62be/uv-0.6.16-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:f75470257c62bd07e3bed37b3a43ed062d9e2c5574612f447cbdc497d8295b22", size = 16214868 }, { url = "https://files.pythonhosted.org/packages/c3/58/886fda363c69ae62ccfd737160d4580ab46354f172340dbcf7d269bc358d/uv-0.6.16-py3-none-musllinux_1_1_i686.whl", hash = "sha256:13366a987a732024c78a395bea7fdb8dc0a6a83827f6808cf7b7e528f6239356", size = 16474287 }, { url = "https://files.pythonhosted.org/packages/e8/fe/9da8e985dbd9737a12011cb6ab8ab832800cec69ec6c59f98821ae75602b/uv-0.6.16-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8ea9e54cc497eb16df87b9e0e41df8f04e9fd4b7ae687097cd706446d10dd14d", size = 17395929 }, { url = "https://files.pythonhosted.org/packages/55/c4/546f760d3b49c7632a95f038536b75f9b7d850c505d1bd31ff9fc2cf5929/uv-0.6.16-py3-none-win32.whl", hash = "sha256:6f73d349dcdfea8f7a88ab1c814fd96392a23cc45cc8481505987938f508f982", size = 16545669 }, { url = "https://files.pythonhosted.org/packages/bc/1c/bcb84be3642f59ad5270e2e9a9395ec6ffab640ce51a43dbe49e30211c1f/uv-0.6.16-py3-none-win_amd64.whl", hash = "sha256:33f4c6b413e3c81d85ccd52bb8a19c11f0587fcbabca731582e0ecded94e1b06", size = 18081915 }, { url = "https://files.pythonhosted.org/packages/ee/da/072c624ece2bcb85bed7590a175bf1029b97659cdb7d0c92e1fc66c507dc/uv-0.6.16-py3-none-win_arm64.whl", hash = "sha256:011f1779536f24d2c46bdc6fe917add943e00a5a45d9ac46be8a281f4ed1c6b7", size = 16784908 }, ] [[package]] name = "virtualenv" version = "20.29.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280 } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390 }, { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386 }, { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017 }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903 }, { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381 }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] [[package]] name = "zipp" version = "3.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, ]