pax_global_header00006660000000000000000000000064146546571050014527gustar00rootroot0000000000000052 comment=1f6316ab40245043a1eecb3f79d0b41124856958 todoist-api-python-2.1.6/000077500000000000000000000000001465465710500153105ustar00rootroot00000000000000todoist-api-python-2.1.6/.flake8000066400000000000000000000001151465465710500164600ustar00rootroot00000000000000[flake8] max-line-length = 88 extend-ignore = E203, W503 max-complexity = 10 todoist-api-python-2.1.6/.github/000077500000000000000000000000001465465710500166505ustar00rootroot00000000000000todoist-api-python-2.1.6/.github/CODEOWNERS000066400000000000000000000000261465465710500202410ustar00rootroot00000000000000* @Doist/Integrations todoist-api-python-2.1.6/.github/ISSUE_TEMPLATE/000077500000000000000000000000001465465710500210335ustar00rootroot00000000000000todoist-api-python-2.1.6/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013451465465710500235300ustar00rootroot00000000000000--- 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-2.1.6/.github/ISSUE_TEMPLATE/enhancement.md000066400000000000000000000010761465465710500236460ustar00rootroot00000000000000--- 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-2.1.6/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000003071465465710500232240ustar00rootroot00000000000000--- name: Question about: Standard template for reporting questions title: '' labels: 'question' assignees: '' --- ## Question description todoist-api-python-2.1.6/.github/renovate.json000066400000000000000000000002231465465710500213630ustar00rootroot00000000000000{ "extends": [ "github>doist/renovate-config:integrations-base", "github>doist/renovate-config:integrations-automerge" ] } todoist-api-python-2.1.6/.github/workflows/000077500000000000000000000000001465465710500207055ustar00rootroot00000000000000todoist-api-python-2.1.6/.github/workflows/ci.yml000066400000000000000000000012021465465710500220160ustar00rootroot00000000000000name: tests on: [pull_request, workflow_dispatch] 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@v3 with: python-version: "3.11" - name: Install dependencies run: | set -ex curl -sSL https://install.python-poetry.org | POETRY_HOME=$HOME/.poetry python3 - --yes $HOME/.poetry/bin/poetry install - name: Test with pytest run: | set -ex $HOME/.poetry/bin/poetry run pytest todoist-api-python-2.1.6/.github/workflows/publish.yml000066400000000000000000000015521465465710500231010ustar00rootroot00000000000000name: 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@v3 with: python-version: "3.11" - name: Install dependencies run: | set -ex curl -sSL https://install.python-poetry.org | POETRY_HOME=$HOME/.poetry python3 - --yes $HOME/.poetry/bin/poetry install - name: Build and publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python -m pip install --upgrade pip pip install setuptools wheel twine build python -m build python -m twine upload dist/* todoist-api-python-2.1.6/.gitignore000066400000000000000000000000721465465710500172770ustar00rootroot00000000000000.vscode .idea .mypy_cache .pytest_cache __pycache__/ dist todoist-api-python-2.1.6/.isort.cfg000066400000000000000000000000331465465710500172030ustar00rootroot00000000000000[settings] profile = black todoist-api-python-2.1.6/.pre-commit-config.yaml000066400000000000000000000015571465465710500216010ustar00rootroot00000000000000default_language_version: python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.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.5.4 hooks: # Run the linter - id: ruff args: ["--fix"] # Run the formatter - id: ruff-format - repo: local hooks: - id: mypy name: mypy entry: poetry run mypy language: system "types_or": [python, pyi] args: ["--scripts-are-modules"] require_serial: true todoist-api-python-2.1.6/.python-version000066400000000000000000000000071465465710500203120ustar00rootroot000000000000003.12.3 todoist-api-python-2.1.6/CHANGELOG.md000066400000000000000000000027701465465710500171270ustar00rootroot00000000000000# 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] ... ## [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-2.1.6/LICENSE000066400000000000000000000020601465465710500163130ustar00rootroot00000000000000The 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-2.1.6/README.md000066400000000000000000000055321465465710500165740ustar00rootroot00000000000000# Todoist API Python Client This is the official Python API client for the Todoist REST API. ### Installation The repository can be included as a [Poetry](https://python-poetry.org/) dependency in `pyproject.toml`. It is best to integrate to a release tag to ensure a stable dependency: ```toml [tool.poetry.dependencies] todoist-api-python = "^v2.0.0" ``` ### Supported Python Versions Python 3.9 is fully supported and tested, and while it may work with other Python 3 versions, we do not test for them. ### Usage An example of initializing the API client and fetching a user's tasks: ```python from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.api import TodoistAPI # Fetch tasks asynchronously async def get_tasks_async(): api = TodoistAPIAsync("YOURTOKEN") try: tasks = await api.get_tasks() print(tasks) except Exception as error: print(error) # Fetch tasks synchronously def get_tasks_sync(): api = TodoistAPI("my token") try: tasks = api.get_tasks() print(tasks) except Exception as error: print(error) ``` Example of paginating through a completed project tasks: ```python def get_all_completed_items(original_params: dict): params = original_params.copy() results = [] while True: response = api.get_completed_items(**(params | {"limit": 100})) results.append(response.items) if not response.has_more: break params["cursor"] = response.next_cursor # flatten the results return [item for sublist in results for item in sublist] items = get_all_completed_items({"project_id": 123}) ``` ### Documentation For more detailed reference documentation, have a look at the [API documentation with Python examples](https://developer.todoist.com/rest/v2/?python). ### Development To install Python dependencies: ```sh $ poetry install ``` To install pre-commit: ```sh $ poetry run pre-commit install ``` You can try your changes via REPL by running: ```sh $ poetry 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, such as bugs, questions, comments, etc. can be reported as *Issues* in this repository, and will be handled by Doist. ### Contributions We would love contributions in the form of *Pull requests* in this repository. todoist-api-python-2.1.6/SECURITY.md000066400000000000000000000010231465465710500170750ustar00rootroot00000000000000# 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-2.1.6/mypy.ini000066400000000000000000000007471465465710500170170ustar00rootroot00000000000000[mypy] python_version = 3.8 follow_imports = silent scripts_are_modules = true namespace_packages = true no_implicit_optional = true # We had to ignore missing imports, because of third-party libraries installed # inside the virtualenv, and apparently there's no easy way for mypy to respect # packages inside the virtualenv. That's the option pre-commit-config runs with # by default, but we add it here as well for the sake of uniformity of the # output ignore_missing_imports = true todoist-api-python-2.1.6/poetry.lock000066400000000000000000000700461465465710500175130ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.6.1" files = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] [[package]] name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.6.0" files = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] [package.extras] unicode-backport = ["unicodedata2"] [[package]] name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] [[package]] name = "distlib" version = "0.3.5" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.8.0" description = "A platform independent file lock." optional = false python-versions = ">=3.7" files = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] [package.extras] docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "identify" version = "2.5.3" description = "File identification library for Python" optional = false python-versions = ">=3.7" files = [ {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" optional = false python-versions = "*" files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] [[package]] name = "mypy" version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] [package.dependencies] setuptools = "*" [[package]] name = "packaging" version = "23.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] [package.extras] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.6" files = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" files = [ {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" version = "0.25.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" files = [ {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, ] [package.dependencies] pyyaml = "*" requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "ruff" version = "0.5.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"}, {file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"}, {file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"}, {file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"}, {file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"}, {file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"}, {file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"}, {file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"}, {file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"}, {file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"}, {file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"}, {file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"}, ] [[package]] name = "setuptools" version = "70.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "types-requests" version = "2.32.0.20240712" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ {file = "types-requests-2.32.0.20240712.tar.gz", hash = "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358"}, {file = "types_requests-2.32.0.20240712-py3-none-any.whl", hash = "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"}, ] [package.dependencies] urllib3 = ">=2" [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.16.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.6" files = [ {file = "virtualenv-20.16.3-py2.py3-none-any.whl", hash = "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1"}, {file = "virtualenv-20.16.3.tar.gz", hash = "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9"}, ] [package.dependencies] distlib = ">=0.3.5,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.9" content-hash = "89f64787261661b096be009d0d6a3ed789d5af990b80934e7c07566e297e1bd6" todoist-api-python-2.1.6/pyproject.toml000066400000000000000000000204551465465710500202320ustar00rootroot00000000000000[tool.poetry] name = "todoist_api_python" version = "2.1.6" description = "Official Python SDK for the Todoist REST API." readme = "README.md" homepage = "https://github.com/Doist/todoist-api-python" repository = "https://github.com/Doist/todoist-api-python" documentation = "https://developer.todoist.com/rest/" authors = ["Doist Developers "] keywords = ["todoist", "rest", "api", "python"] license = "MIT" classifiers = [ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", ] include = [ "LICENSE", ] [tool.poetry.dependencies] python = "^3.9" requests = "^2.32.3" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" pre-commit = "^3.8.0" mypy = "^1.11" responses = "^0.25.3" pytest-asyncio = "^0.21.0" types-requests = "^2.32" ruff = "^0.5.6" [build-system] requires = ["poetry-core>=1.0.8"] build-backend = "poetry.core.masonry.api" [tool.mypy] python_version = 3.11 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.mypy.overrides]] module = [ ] ignore_missing_imports = true [[tool.mypy.overrides]] module = [] disallow_untyped_defs = true warn_unreachable = true [tool.pydantic-mypy] init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.ruff] target-version = "py311" # 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", "W", # pycodestyle "F", # pyflakes "I", # isort "PL", # pylint "RUF", # ruff "S", # flake8-bandit "T20", # flake8-print "SIM", # flake8-simplify "UP", # pyupgrade "TCH", # flake8-type-checking "TRY", # tryceratops "BLE", # flake8-blind-except "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 ] ignore = [ ## D - pydocstyle ## # D1XX errors are OK. Don't force people into over-documenting. "D100", "D101", "D102", "D103", "D104", "D105", "D107", # These need to be fixed. "D205", "D400", "D401", ## E / W - pycodestyle ## "E501", # line too long ## PL - pylint ## # Commented-out rules are rules that we disable in pylint but are not supported by ruff yet. "PLR6301", # no-self-use "PLC2701", # import-private-name # Import order issues # "PLC0411", # wrong-import-order # "PLC0412", # wrong-import-position "PLC0415", # import-outside-top-level # flake8-fixme "FIX001", # line-contains-fixme: We allow FIXME but not TODO, XXX, HACK "FIX002", # line-contains-todo: Rule matches "todoist" which we use a lot in comments. # flake8-implicit-str-concat "ISC001", # May conflict with the formatter # Documentation issues # "C0114", # missing-module-docstring # Complexity issues "PLR0904", # too-many-public-methods # "PLC0302", # too-many-lines "PLR1702", # too-many-nested-blocks # "PLR0902", # too-many-instance-attributes "PLR0911", # too-many-return-statements "PLR0915", # too-many-statements "PLR0912", # too-many-branches # "PLR0903", # too-few-public-methods "PLR0914", # too-many-locals # "PLC0301", # line-too-long "PLR0913", # too-many-arguments "PLR0917", # too-many-positional "PLR2004", # magic-value-comparison "PLW0603", # global-statement "PLW2901", # redefined-loop-name ## RUF - ruff ## "RUF001", # ambiguous-unicode-character-string "RUF002", # ambiguous-unicode-character-docstring "RUF003", # ambiguous-unicode-character-comment "RUF012", # mutable-class-default # Enable when Poetry supports PEP 621 and we migrate our confguration to it. # See: https://github.com/python-poetry/poetry-core/pull/567 "RUF200", "S101", # assert "S104", # hardcoded-bind-all-interfaces "S105", # hardcoded-password-string "S106", # hardcoded-password-func-arg "S303", # suspicious-insecure-hash-usage "S310", # suspicious-url-open-usage "S311", # suspicious-non-cryptographic-random-usage "S324", # hashlib-insecure-hash-function "S603", # subprocess-without-shell-equals-true "S607", # start-process-with-partial-path "S608", # hardcoded-sql-expression ## DTZ - flake8-datetimez "DTZ001", # call-datetime-without-tzinfo "DTZ002", # call-datetime-today "DTZ003", # call-datetime-utcnow "DTZ004", # call-datetime-utcfromtimestamp "DTZ005", # call-datetime-now-without-tzinfo "DTZ006", # call-datetime-fromtimestamp "DTZ007", # call-datetime-strptime-without-zone "DTZ011", # call-date-today ## SIM - flake8-simplify ## "SIM102", # collapsible-if "SIM117", # multiple-with-statements # Enable when the rule is out of preview and false-positives are handled. # See: https://docs.astral.sh/ruff/rules/in-dict-keys/ "SIM118", # in-dict-keys ## TRY - tryceratops ## "TRY003", # raise-vanilla-args "TRY004", # type-check-without-type-error "TRY301", # raise-within-try ## RET - flake8-return ## "RET504", # unnecessary-assign ## PT - flake8-pytest-style ## "PT004", # pytest-missing-fixture-name-underscore "PT012", # pytest-raises-with-multiple-statements ## UP - pyupgrade ## "UP038", # non-pep604-isinstance ## B - flake8-bugbear ## "B008", # function-call-in-default-argument "B009", # get-attr-with-constant "B010", # set-attr-with-constant "B018", # useless-expression ## PTH - flake8-user-pathlib # "PTH118", # os-path-join "PTH120", # os-path-dirname "PTH122", # os-path-splitext "PTH123", # builtin-open "PTH207", # glob ## TID - flake8-tidy-imports ## "TID252", # relative-imports ## N - pep8-naming ## "N801", # invalid-class-name "N802", # invalid-function-name "N803", # invalid-argument-name "N815", # mixed-case-variable-in-class-scope # Broken in ruff 0.5.0 upgrade "SIM103", # needless-bool "PERF403", # manual-dict-comprehension ] flake8-pytest-style.fixture-parentheses = false flake8-pytest-style.mark-parentheses = false pylint.allow-dunder-method-names = [ "__json__", ] [tool.ruff.lint.flake8-builtins] builtins-ignorelist = [ "id", # 59 "filter", # 10 "type", # 4 "input", # 2 "format", # 2 "hash", # 1 "help", # 1 ] [tool.ruff.lint.per-file-ignores] # These files have only a bunch of imports in them to force code loading. "tests/**" = [ "S101", # assert "S104", # hardcoded-bind-all-interfaces "S105", # hardcoded-password-string "S106", # hardcoded-password-func-arg "S107", # hardcoded-password-default "S301", # suspicious-pickle-usage "RUF018", # assignment-in-assert ] # To import select fixtures from non-local conftests. # Importing and using the fixture makes it be shadowed. "test_*.py" = ["F811", "PLC0414"] [tool.ruff.lint.isort] section-order = [ "future", "standard-library", "third-party", "parts", "first-party", "td-models", "td-apps", "local-folder", ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] "parts.web.validators" = "v" [tool.ruff.lint.pydocstyle] convention = "pep257" [tool.ruff.lint.pyupgrade] # Required by tools like Pydantic that use type information at runtime. # https://github.com/asottile/pyupgrade/issues/622#issuecomment-1088766572 keep-runtime-typing = true [tool.ruff.format] docstring-code-format = true todoist-api-python-2.1.6/tests/000077500000000000000000000000001465465710500164525ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/__init__.py000066400000000000000000000000001465465710500205510ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/conftest.py000066400000000000000000000107101465465710500206500ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.quick_add_responses import QUICK_ADD_RESPONSE_FULL from tests.data.test_defaults import ( DEFAULT_AUTH_RESPONSE, DEFAULT_COLLABORATORS_RESPONSE, DEFAULT_COMMENT_RESPONSE, DEFAULT_COMMENTS_RESPONSE, DEFAULT_COMPLETED_ITEMS_RESPONSE, DEFAULT_LABEL_RESPONSE, DEFAULT_LABELS_RESPONSE, DEFAULT_PROJECT_RESPONSE, DEFAULT_PROJECTS_RESPONSE, DEFAULT_SECTION_RESPONSE, DEFAULT_SECTIONS_RESPONSE, DEFAULT_TASK_RESPONSE, DEFAULT_TASKS_RESPONSE, DEFAULT_TOKEN, ) from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import ( AuthResult, Collaborator, Comment, CompletedItems, Label, Project, QuickAddResult, 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() -> Task: return Task.from_dict(DEFAULT_TASK_RESPONSE) @pytest.fixture def default_tasks_response() -> list[dict[str, Any]]: return DEFAULT_TASKS_RESPONSE @pytest.fixture def default_tasks_list() -> list[Task]: return [Task.from_dict(obj) for obj in DEFAULT_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[dict[str, Any]]: return DEFAULT_PROJECTS_RESPONSE @pytest.fixture def default_projects_list() -> list[Project]: return [Project.from_dict(obj) for obj in DEFAULT_PROJECTS_RESPONSE] @pytest.fixture def default_collaborators_response() -> list[dict[str, Any]]: return DEFAULT_COLLABORATORS_RESPONSE @pytest.fixture def default_collaborators_list() -> list[Collaborator]: return [Collaborator.from_dict(obj) for obj 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[dict[str, Any]]: return DEFAULT_SECTIONS_RESPONSE @pytest.fixture def default_sections_list() -> list[Section]: return [Section.from_dict(obj) for obj 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[dict[str, Any]]: return DEFAULT_COMMENTS_RESPONSE @pytest.fixture def default_comments_list() -> list[Comment]: return [Comment.from_dict(obj) for obj 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[dict[str, Any]]: return DEFAULT_LABELS_RESPONSE @pytest.fixture def default_labels_list() -> list[Label]: return [Label.from_dict(obj) for obj in DEFAULT_LABELS_RESPONSE] @pytest.fixture def default_quick_add_response() -> dict[str, Any]: return QUICK_ADD_RESPONSE_FULL @pytest.fixture def default_quick_add_result() -> QuickAddResult: return QuickAddResult.from_quick_add_response(QUICK_ADD_RESPONSE_FULL) @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) @pytest.fixture def default_completed_items_response() -> dict[str, Any]: return DEFAULT_COMPLETED_ITEMS_RESPONSE @pytest.fixture def default_completed_items() -> CompletedItems: return CompletedItems.from_dict(DEFAULT_COMPLETED_ITEMS_RESPONSE) todoist-api-python-2.1.6/tests/data/000077500000000000000000000000001465465710500173635ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/data/__init__.py000066400000000000000000000000001465465710500214620ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/data/quick_add_responses.py000066400000000000000000000051571465465710500237720ustar00rootroot00000000000000from __future__ import annotations from typing import Any QUICK_ADD_RESPONSE_MINIMAL: dict[str, Any] = { "added_by_uid": "21180723", "assigned_by_uid": None, "checked": 0, "child_order": 6, "collapsed": 0, "content": "some task", "description": "", "added_at": "2021-02-05T11:02:56.00000Z", "date_completed": None, "due": None, "id": "4554989047", "in_history": 0, "is_deleted": 0, "labels": [], "meta": { "assignee": [None, None], "due": { "date_local": None, "datetime_local": None, "datetime_utc": None, "is_recurring": False, "lang": None, "object_type": "null", "string": None, "timezone": None, "timezone_name": None, }, "labels": {}, "priority": 1, "project": [None, None], "section": [None, None], "text": "some task", }, "parent_id": None, "priority": 1, "project_id": "2203108698", "responsible_uid": None, "section_id": None, "sync_id": None, "user_id": "21180723", } QUICK_ADD_RESPONSE_FULL: dict[str, Any] = { "added_by_uid": "21180723", "assigned_by_uid": "21180723", "checked": 0, "child_order": 1, "collapsed": 0, "content": "some task", "description": "a description", "added_at": "2021-02-05T11:04:54.00000Z", "date_completed": None, "due": { "date": "2021-02-06T11:00:00.00000Z", "is_recurring": False, "lang": "en", "string": "Feb 6 11:00 AM", "timezone": "Europe/London", }, "id": "4554993687", "in_history": 0, "is_deleted": 0, "labels": ["Label1", "Label2"], "meta": { "assignee": ["29172386", "Some Guy"], "due": { "date_local": "2021-02-06T00:00:00.00000Z", "datetime_local": "2021-02-06T11:00:00.00000Z", "datetime_utc": "2021-02-06T11:00:00.00000Z", "is_recurring": False, "lang": "en", "object_type": "fixed_datetime", "string": "Feb 6 11:00 AM", "timezone": {"zone": "Europe/London"}, "timezone_name": "Europe/London", }, "labels": {"2156154810": "Label1", "2156154812": "Label2"}, "priority": 1, "project": ["2257514220", "test"], "section": ["2232454220", "A section"], "text": "some task", }, "parent_id": None, "priority": 1, "project_id": "2257514220", "responsible_uid": "29172386", "section_id": "2232454220", "sync_id": "4554993687", "user_id": "21180723", } todoist-api-python-2.1.6/tests/data/test_defaults.py000066400000000000000000000110271465465710500226040ustar00rootroot00000000000000from __future__ import annotations from typing import Any REST_API_BASE_URL = "https://api.todoist.com/rest/v2" SYNC_API_BASE_URL = "https://api.todoist.com/sync/v9" AUTH_BASE_URL = "https://todoist.com" DEFAULT_TOKEN = "A TOKEN" DEFAULT_REQUEST_ID = "REQUEST12345" DEFAULT_DUE_RESPONSE = { "date": "2016-09-01", "is_recurring": True, "datetime": "2016-09-01T09:00:00.00000Z", "string": "tomorrow at 12", "timezone": "Europe/Moscow", } DEFAULT_DURATION_RESPONSE = { "amount": 60, "unit": "minute", } DEFAULT_TASK_RESPONSE = { "id": "1234", "assigner_id": "2971358", "assignee_id": "2423523", "project_id": "2203306141", "parent_id": "8686843758", "section_id": "7025", "order": 3, "content": "Some Task Content", "description": "A description", "is_completed": False, "is_shared": False, "labels": [], "priority": 1, "comment_count": 0, "creator_id": "0", "created_at": "2019-01-02T21:00:30.00000Z", "url": "https://todoist.com/showTask?id=2995104339", "due": DEFAULT_DUE_RESPONSE, "duration": DEFAULT_DURATION_RESPONSE, } DEFAULT_TASK_RESPONSE_2 = dict(DEFAULT_TASK_RESPONSE) DEFAULT_TASK_RESPONSE_2["id"] = "5678" DEFAULT_TASKS_RESPONSE = [ DEFAULT_TASK_RESPONSE, DEFAULT_TASK_RESPONSE_2, ] DEFAULT_PROJECT_RESPONSE = { "id": "1234", "name": "Inbox", "comment_count": 10, "order": 1, "color": "red", "is_shared": False, "parent_id": "5678", "is_favorite": False, "is_inbox_project": True, "is_team_inbox": True, "can_assign_tasks": False, "url": "https://todoist.com/showProject?id=1234", "view_style": "list", } DEFAULT_PROJECT_RESPONSE_2 = dict(DEFAULT_PROJECT_RESPONSE) DEFAULT_PROJECT_RESPONSE_2["id"] = "5678" DEFAULT_PROJECTS_RESPONSE = [ DEFAULT_PROJECT_RESPONSE, DEFAULT_PROJECT_RESPONSE_2, ] DEFAULT_COLLABORATOR_RESPONSE = { "id": "1234", "name": "Alice", "email": "alice@example.com", } DEFAULT_COLLABORATOR_RESPONSE_2 = dict(DEFAULT_COLLABORATOR_RESPONSE) DEFAULT_COLLABORATOR_RESPONSE_2["id"] = "5678" DEFAULT_COLLABORATORS_RESPONSE = [ DEFAULT_COLLABORATOR_RESPONSE, DEFAULT_COLLABORATOR_RESPONSE_2, ] DEFAULT_SECTION_RESPONSE = { "id": "1234", "project_id": "4567", "name": "A Section", "order": 1, } DEFAULT_SECTION_RESPONSE_2 = dict(DEFAULT_SECTION_RESPONSE) DEFAULT_SECTION_RESPONSE_2["id"] = 5678 DEFAULT_SECTIONS_RESPONSE = [ DEFAULT_SECTION_RESPONSE, DEFAULT_SECTION_RESPONSE_2, ] 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": 1234, "image_height": 5678, "url": "https://todoist.com", "title": "Todoist Website", } DEFAULT_COMMENT_RESPONSE: dict[str, Any] = { "id": "1234", "content": "A comment", "posted_at": "2016-09-22T07:00:00.00000Z", "task_id": "2345", "project_id": "4567", "attachment": DEFAULT_ATTACHMENT_RESPONSE, } DEFAULT_COMMENT_RESPONSE_2 = dict(DEFAULT_COMMENT_RESPONSE) DEFAULT_COMMENT_RESPONSE_2["id"] = "5678" DEFAULT_COMMENT_RESPONSE_2["attachment"] = None DEFAULT_COMMENTS_RESPONSE = [ DEFAULT_COMMENT_RESPONSE, DEFAULT_COMMENT_RESPONSE_2, ] 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_LABELS_RESPONSE = [ DEFAULT_LABEL_RESPONSE, DEFAULT_LABEL_RESPONSE_2, ] DEFAULT_AUTH_RESPONSE = { "access_token": "1234", "state": "somestate", } DEFAULT_ITEM_RESPONSE = { "id": "2995104339", "user_id": "2671355", "project_id": "2203306141", "content": "Buy Milk", "description": "", "priority": 1, "due": DEFAULT_DUE_RESPONSE, "child_order": 1, "day_order": -1, "collapsed": False, "labels": ["Food", "Shopping"], "added_by_uid": "2671355", "assigned_by_uid": "2671355", "checked": False, "is_deleted": False, "added_at": "2014-09-26T08:25:05.000000Z", } DEFAULT_ITEM_COMPLETED_INFO_RESPONSE = {"item_id": "2995104339", "completed_items": 12} DEFAULT_COMPLETED_ITEMS_RESPONSE = { "items": [DEFAULT_ITEM_RESPONSE], "completed_info": [DEFAULT_ITEM_COMPLETED_INFO_RESPONSE], "total": 22, "next_cursor": "k85gVI5ZAs8AAAABFoOzAQ", "has_more": True, } todoist-api-python-2.1.6/tests/test_api_async.py000066400000000000000000000016161465465710500220350ustar00rootroot00000000000000from __future__ import annotations from unittest.mock import MagicMock, patch import requests from tests.data.test_defaults import DEFAULT_TOKEN from tests.utils.test_utils import get_todoist_api_patch from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync @patch(get_todoist_api_patch(TodoistAPI.__init__)) def test_constructs_api_with_token(sync_api_constructor: MagicMock): sync_api_constructor.return_value = None TodoistAPIAsync(DEFAULT_TOKEN) sync_api_constructor.assert_called_once_with(DEFAULT_TOKEN, None) @patch(get_todoist_api_patch(TodoistAPI.__init__)) def test_constructs_api_with_token_and_session(sync_api_constructor: MagicMock): sync_api_constructor.return_value = None session = requests.Session() TodoistAPIAsync(DEFAULT_TOKEN, session) sync_api_constructor.assert_called_once_with(DEFAULT_TOKEN, session) todoist-api-python-2.1.6/tests/test_api_comments.py000066400000000000000000000132261465465710500225450ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_REQUEST_ID, REST_API_BASE_URL from tests.utils.test_utils import assert_auth_header, assert_request_id_header 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, ): comment_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/comments/{comment_id}" requests_mock.add( responses.GET, expected_endpoint, json=default_comment_response, status=200, ) comment = todoist_api.get_comment(comment_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert comment == default_comment comment = await todoist_api_async.get_comment(comment_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) 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[dict[str, Any]], default_comments_list: list[Comment], ): task_id = "1234" requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/comments?task_id={task_id}", json=default_comments_response, status=200, ) comments = todoist_api.get_comments(task_id=task_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert comments == default_comments_list comments = await todoist_api_async.get_comments(task_id=task_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert comments == default_comments_list @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, ): content = "A Comment" project_id = 123 attachment_data = { "resource_type": "file", "file_url": "https://s3.amazonaws.com/domorebetter/Todoist+Setup+Guide.pdf", "file_type": "application/pdf", "file_name": "File.pdf", } expected_payload: dict[str, Any] = { "content": content, "project_id": project_id, "attachment": attachment_data, } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/comments", json=default_comment_response, status=200, ) new_comment = todoist_api.add_comment( content=content, project_id=project_id, attachment=attachment_data, request_id=DEFAULT_REQUEST_ID, ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_comment == default_comment new_comment = await todoist_api_async.add_comment( content=content, project_id=project_id, attachment=attachment_data, request_id=DEFAULT_REQUEST_ID, ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert new_comment == default_comment @pytest.mark.asyncio async def test_update_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): comment_id = "1234" args = { "content": "An updated comment", } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/comments/{comment_id}", status=204 ) response = todoist_api.update_comment( comment_id=comment_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(args) assert response is True response = await todoist_api_async.update_comment( comment_id=comment_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(args) assert response is True @pytest.mark.asyncio async def test_delete_comment( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): comment_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/comments/{comment_id}" requests_mock.add( responses.DELETE, expected_endpoint, status=204, ) response = todoist_api.delete_comment(comment_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.delete_comment(comment_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True todoist-api-python-2.1.6/tests/test_api_items.py000066400000000000000000000042671465465710500220460ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from urllib.parse import parse_qs, urlparse import pytest import responses from tests.data.test_defaults import SYNC_API_BASE_URL from tests.utils.test_utils import assert_auth_header from todoist_api_python.endpoints import COMPLETED_ITEMS_ENDPOINT if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import CompletedItems @pytest.mark.asyncio async def test_get_completed_items( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_completed_items_response: dict[str, Any], default_completed_items: CompletedItems, ) -> None: project_id = "1234" section_id = "5678" item_id = "90ab" last_seen_id = "cdef" limit = 30 cursor = "ghij" def assert_query(url): queries = parse_qs(urlparse(url).query) assert queries.get("project_id") == [project_id] assert queries.get("section_id") == [section_id] assert queries.get("item_id") == [item_id] assert queries.get("last_seen_id") == [last_seen_id] assert queries.get("limit") == [str(limit)] assert queries.get("cursor") == [cursor] expected_endpoint = f"{SYNC_API_BASE_URL}/{COMPLETED_ITEMS_ENDPOINT}" requests_mock.add( responses.GET, expected_endpoint, json=default_completed_items_response, status=200, ) completed_items = todoist_api.get_completed_items( project_id, section_id, item_id, last_seen_id, limit, cursor ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_query(requests_mock.calls[0].request.url) assert completed_items == default_completed_items completed_items = await todoist_api_async.get_completed_items( project_id, section_id, item_id, last_seen_id, limit, cursor ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_query(requests_mock.calls[1].request.url) assert completed_items == default_completed_items todoist-api-python-2.1.6/tests/test_api_labels.py000066400000000000000000000146321465465710500221640ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_REQUEST_ID, REST_API_BASE_URL from tests.utils.test_utils import assert_auth_header, assert_request_id_header 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, ): label_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/labels/{label_id}" requests_mock.add( responses.GET, expected_endpoint, json=default_label_response, status=200, ) label = todoist_api.get_label(label_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert label == default_label label = await todoist_api_async.get_label(label_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) 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[dict[str, Any]], default_labels_list: list[Label], ): requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/labels", json=default_labels_response, status=200, ) labels = todoist_api.get_labels() assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert labels == default_labels_list labels = await todoist_api_async.get_labels() assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert labels == default_labels_list @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, ): label_name = "A Label" expected_payload = {"name": label_name} requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/labels", json=default_label_response, status=200, ) new_label = todoist_api.add_label(name=label_name, request_id=DEFAULT_REQUEST_ID) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_label == default_label new_label = await todoist_api_async.add_label( name=label_name, request_id=DEFAULT_REQUEST_ID ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) 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, ): label_name = "A Label" optional_args = { "color": 30, "order": 3, "favorite": True, } expected_payload: dict[str, Any] = {"name": label_name} expected_payload.update(optional_args) requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/labels", json=default_label_response, status=200, ) new_label = todoist_api.add_label( name=label_name, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_label == default_label new_label = await todoist_api_async.add_label( name=label_name, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert new_label == default_label @pytest.mark.asyncio async def test_update_label( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): label_id = "123" args = { "name": "An updated label", "order": 2, "color": 31, "favorite": False, } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/labels/{label_id}", status=204 ) response = todoist_api.update_label( label_id=label_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(args) assert response is True response = await todoist_api_async.update_label( label_id=label_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(args) assert response is True @pytest.mark.asyncio async def test_delete_label( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): label_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/labels/{label_id}" requests_mock.add( responses.DELETE, expected_endpoint, status=204, ) response = todoist_api.delete_label(label_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.delete_label(label_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True todoist-api-python-2.1.6/tests/test_api_projects.py000066400000000000000000000170541465465710500225540ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_REQUEST_ID, REST_API_BASE_URL from tests.utils.test_utils import assert_auth_header, assert_request_id_header if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import 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, ): project_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/projects/{project_id}" requests_mock.add( responses.GET, expected_endpoint, json=default_project_response, status=200, ) project = todoist_api.get_project(project_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert project == default_project project = await todoist_api_async.get_project(project_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) 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[dict[str, Any]], default_projects_list: list[Project], ): requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/projects", json=default_projects_response, status=200, ) projects = todoist_api.get_projects() assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert projects == default_projects_list projects = await todoist_api_async.get_projects() assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert projects == default_projects_list @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, ): project_name = "A Project" expected_payload = {"name": project_name} requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/projects", json=default_project_response, status=200, ) new_project = todoist_api.add_project( name=project_name, request_id=DEFAULT_REQUEST_ID ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_project == default_project new_project = await todoist_api_async.add_project( name=project_name, request_id=DEFAULT_REQUEST_ID ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) 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, ): project_name = "A Project" optional_args = { "parent_id": 789, "color": 30, "order": 3, "favorite": True, } expected_payload: dict[str, Any] = {"name": project_name} expected_payload.update(optional_args) requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/projects", json=default_project_response, status=200, ) new_project = todoist_api.add_project( name=project_name, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_project == default_project new_project = await todoist_api_async.add_project( name=project_name, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert new_project == default_project @pytest.mark.asyncio async def test_update_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): project_id = "123" args = { "name": "An updated project", "color": 31, "favorite": False, } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/projects/{project_id}", status=204 ) response = todoist_api.update_project( project_id=project_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(args) assert response is True response = await todoist_api_async.update_project( project_id=project_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(args) assert response is True @pytest.mark.asyncio async def test_delete_project( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): project_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/projects/{project_id}" requests_mock.add( responses.DELETE, expected_endpoint, status=204, ) response = todoist_api.delete_project(project_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.delete_project(project_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) 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[dict[str, Any]], default_collaborators_list: list[Project], ): project_id = "123" expected_endpoint = f"{REST_API_BASE_URL}/projects/{project_id}/collaborators" requests_mock.add( responses.GET, expected_endpoint, json=default_collaborators_response, status=200, ) collaborators = todoist_api.get_collaborators(project_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert collaborators == default_collaborators_list collaborators = await todoist_api_async.get_collaborators(project_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert collaborators == default_collaborators_list todoist-api-python-2.1.6/tests/test_api_sections.py000066400000000000000000000136551465465710500225550ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any import pytest import responses from tests.data.test_defaults import DEFAULT_REQUEST_ID, REST_API_BASE_URL from tests.utils.test_utils import assert_auth_header, assert_request_id_header 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, ): section_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/sections/{section_id}" requests_mock.add( responses.GET, expected_endpoint, json=default_section_response, status=200, ) section = todoist_api.get_section(section_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert section == default_section section = await todoist_api_async.get_section(section_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert section == default_section @pytest.mark.asyncio async def test_get_all_sections( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_sections_response: list[dict[str, Any]], default_sections_list: list[Section], ): requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/sections", json=default_sections_response, status=200, ) sections = todoist_api.get_sections() assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert sections == default_sections_list sections = await todoist_api_async.get_sections() assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert sections == default_sections_list @pytest.mark.asyncio async def test_get_project_sections( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_sections_response: list[dict[str, Any]], ): project_id = "123" requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/sections?project_id={project_id}", json=default_sections_response, status=200, ) todoist_api.get_sections(project_id=project_id) await todoist_api_async.get_sections(project_id=project_id) assert len(requests_mock.calls) == 2 @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, ): section_name = "A Section" project_id = "123" order = 3 expected_payload: dict[str, Any] = { "name": section_name, "project_id": project_id, "order": order, } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/sections", json=default_section_response, status=200, ) new_section = todoist_api.add_section( name=section_name, project_id=project_id, order=order, request_id=DEFAULT_REQUEST_ID, ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_section == default_section new_section = await todoist_api_async.add_section( name=section_name, project_id=project_id, order=order, request_id=DEFAULT_REQUEST_ID, ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert new_section == default_section @pytest.mark.asyncio async def test_update_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): section_id = "123" args = { "name": "An updated section", } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/sections/{section_id}", status=204 ) response = todoist_api.update_section( section_id=section_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(args) assert response is True response = await todoist_api_async.update_section( section_id=section_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(args) assert response is True @pytest.mark.asyncio async def test_delete_section( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): section_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/sections/{section_id}" requests_mock.add( responses.DELETE, expected_endpoint, status=204, ) response = todoist_api.delete_section(section_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.delete_section(section_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True todoist-api-python-2.1.6/tests/test_api_tasks.py000066400000000000000000000252011465465710500220410ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any from urllib.parse import quote import pytest import responses from tests.data.test_defaults import ( DEFAULT_REQUEST_ID, REST_API_BASE_URL, SYNC_API_BASE_URL, ) from tests.utils.test_utils import assert_auth_header, assert_request_id_header if TYPE_CHECKING: from todoist_api_python.api import TodoistAPI from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.models import QuickAddResult, 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, ): task_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/tasks/{task_id}" requests_mock.add( responses.GET, expected_endpoint, json=default_task_response, status=200, ) task = todoist_api.get_task(task_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert task == default_task task = await todoist_api_async.get_task(task_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert task == default_task @pytest.mark.asyncio async def test_get_tasks_minimal( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_tasks_response: list[dict[str, Any]], default_tasks_list: list[Task], ): requests_mock.add( responses.GET, f"{REST_API_BASE_URL}/tasks", json=default_tasks_response, status=200, ) tasks = todoist_api.get_tasks() assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert tasks == default_tasks_list tasks = await todoist_api_async.get_tasks() assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert tasks == default_tasks_list @pytest.mark.asyncio async def test_get_tasks_full( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_tasks_response: list[dict[str, Any]], default_tasks_list: list[Task], ): project_id = "1234" label_id = 2345 filter = "today" lang = "en" ids = [1, 2, 3, 4] encoded_ids = quote(",".join(str(x) for x in ids)) expected_endpoint = ( f"{REST_API_BASE_URL}/tasks" f"?project_id={project_id}&label_id={label_id}" f"&filter={filter}&lang={lang}&ids={encoded_ids}" ) requests_mock.add( responses.GET, expected_endpoint, json=default_tasks_response, status=200 ) tasks = todoist_api.get_tasks( project_id=project_id, label_id=label_id, filter=filter, lang=lang, ids=ids ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert tasks == default_tasks_list tasks = await todoist_api_async.get_tasks( project_id=project_id, label_id=label_id, filter=filter, lang=lang, ids=ids ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert tasks == default_tasks_list @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, ): task_content = "Some content" expected_payload = {"content": task_content} requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/tasks", json=default_task_response, status=200, ) new_task = todoist_api.add_task(content=task_content, request_id=DEFAULT_REQUEST_ID) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_task == default_task new_task = await todoist_api_async.add_task( content=task_content, request_id=DEFAULT_REQUEST_ID ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) 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, ): task_content = "Some content" optional_args = { "description": "A description", "project_id": 123, "section_id": 456, "parent_id": 789, "order": 3, "label_ids": [123, 456], "priority": 4, "due_string": "today", "due_date": "2021-01-01", "due_datetime": "2021-01-01T11:00:00Z", "due_lang": "ja", "assignee": 321, } expected_payload: dict[str, Any] = {"content": task_content} expected_payload.update(optional_args) requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/tasks", json=default_task_response, status=200, ) new_task = todoist_api.add_task( content=task_content, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert new_task == default_task new_task = await todoist_api_async.add_task( content=task_content, request_id=DEFAULT_REQUEST_ID, **optional_args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert new_task == default_task @pytest.mark.asyncio async def test_update_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): task_id = "123" args = { "content": "Some updated content", "description": "An updated description", "label_ids": ["123", "456"], "priority": 4, "due_string": "today", "due_date": "2021-01-01", "due_datetime": "2021-01-01T11:00:00Z", "due_lang": "ja", "assignee": "321", } requests_mock.add( responses.POST, f"{REST_API_BASE_URL}/tasks/{task_id}", status=204 ) response = todoist_api.update_task( task_id=task_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert_request_id_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(args) assert response is True response = await todoist_api_async.update_task( task_id=task_id, request_id=DEFAULT_REQUEST_ID, **args ) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert_request_id_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(args) assert response is True @pytest.mark.asyncio async def test_close_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): task_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/tasks/{task_id}/close" requests_mock.add( responses.POST, expected_endpoint, status=204, ) response = todoist_api.close_task(task_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.close_task(task_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True @pytest.mark.asyncio async def test_reopen_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): task_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/tasks/{task_id}/reopen" requests_mock.add( responses.POST, expected_endpoint, status=204, ) response = todoist_api.reopen_task(task_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.reopen_task(task_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True @pytest.mark.asyncio async def test_delete_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, ): task_id = "1234" expected_endpoint = f"{REST_API_BASE_URL}/tasks/{task_id}" requests_mock.add( responses.DELETE, expected_endpoint, status=204, ) response = todoist_api.delete_task(task_id) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert response is True response = await todoist_api_async.delete_task(task_id) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert response is True @pytest.mark.asyncio async def test_quick_add_task( todoist_api: TodoistAPI, todoist_api_async: TodoistAPIAsync, requests_mock: responses.RequestsMock, default_quick_add_response: dict[str, Any], default_quick_add_result: QuickAddResult, ): text = "some task" expected_payload = {"text": text, "meta": True, "auto_reminder": True} requests_mock.add( responses.POST, f"{SYNC_API_BASE_URL}/quick/add", json=default_quick_add_response, status=200, ) response = todoist_api.quick_add_task(text=text) assert len(requests_mock.calls) == 1 assert_auth_header(requests_mock.calls[0].request) assert requests_mock.calls[0].request.body == json.dumps(expected_payload) assert response == default_quick_add_result response = await todoist_api_async.quick_add_task(text=text) assert len(requests_mock.calls) == 2 assert_auth_header(requests_mock.calls[1].request) assert requests_mock.calls[1].request.body == json.dumps(expected_payload) assert response == default_quick_add_result todoist-api-python-2.1.6/tests/test_authentication.py000066400000000000000000000052661465465710500231130ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any from urllib.parse import quote import pytest import responses from tests.data.test_defaults import AUTH_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, ) from todoist_api_python.endpoints import SYNC_API if TYPE_CHECKING: from todoist_api_python.models import AuthResult def test_get_authentication_url(): 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"{AUTH_BASE_URL}/oauth/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, ): client_id = "123" client_secret = "456" code = "789" expected_payload = json.dumps( {"client_id": client_id, "client_secret": client_secret, "code": code} ) requests_mock.add( responses.POST, f"{AUTH_BASE_URL}/oauth/access_token", json=default_auth_response, status=200, ) auth_result = get_auth_token(client_id, client_secret, code) assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.body == expected_payload 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 requests_mock.calls[1].request.body == expected_payload assert auth_result == default_auth_result @pytest.mark.asyncio async def test_revoke_auth_token( requests_mock: responses.RequestsMock, ): client_id = "123" client_secret = "456" token = "AToken" expected_payload = json.dumps( {"client_id": client_id, "client_secret": client_secret, "access_token": token} ) requests_mock.add( responses.POST, f"{SYNC_API}access_tokens/revoke", status=204, ) result = revoke_auth_token(client_id, client_secret, token) assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.body == expected_payload assert result is True result = await revoke_auth_token_async(client_id, client_secret, token) assert len(requests_mock.calls) == 2 assert requests_mock.calls[1].request.body == expected_payload assert result is True todoist-api-python-2.1.6/tests/test_headers.py000066400000000000000000000012401465465710500214730ustar00rootroot00000000000000from __future__ import annotations from todoist_api_python.headers import create_headers def test_create_headers_none(): headers = create_headers() assert headers == {} def test_create_headers_authorization(): token = "A Token" headers = create_headers(token=token) assert headers["Authorization"] == f"Bearer {token}" def test_create_headers_request_id(): request_id = "12345" headers = create_headers(request_id=request_id) assert headers["X-Request-Id"] == request_id def test_create_headers_content_type(): headers = create_headers(with_content=True) assert headers["Content-Type"] == "application/json; charset=utf-8" todoist-api-python-2.1.6/tests/test_http_requests.py000066400000000000000000000066071465465710500230060ustar00rootroot00000000000000from __future__ import annotations import json from typing import Any import pytest import responses from requests import HTTPError, Session from tests.conftest import DEFAULT_TOKEN from todoist_api_python.endpoints import BASE_URL, TASKS_ENDPOINT from todoist_api_python.http_requests import delete, get, post DEFAULT_URL = f"{BASE_URL}/{TASKS_ENDPOINT}" @responses.activate def test_get_with_params(default_task_response: dict[str, Any]): params = {"param1": "value1", "param2": "value2"} responses.add( responses.GET, DEFAULT_URL, json=default_task_response, status=200, ) response = get(Session(), DEFAULT_URL, DEFAULT_TOKEN, params) assert len(responses.calls) == 1 assert ( responses.calls[0].request.url == f"{DEFAULT_URL}?param1=value1¶m2=value2" ) assert ( responses.calls[0].request.headers["Authorization"] == f"Bearer {DEFAULT_TOKEN}" ) assert response == default_task_response @responses.activate def test_get_raise_for_status(): with pytest.raises(HTTPError): responses.add( responses.GET, DEFAULT_URL, status=500, ) get(Session(), DEFAULT_URL, DEFAULT_TOKEN) @responses.activate def test_post_with_data(default_task_response: dict[str, Any]): request_id = "12345" data = {"param1": "value1", "param2": "value2", "request_id": request_id} responses.add( responses.POST, DEFAULT_URL, json=default_task_response, status=200, ) response = post(Session(), DEFAULT_URL, DEFAULT_TOKEN, data) assert len(responses.calls) == 1 assert responses.calls[0].request.url == DEFAULT_URL assert ( responses.calls[0].request.headers["Authorization"] == f"Bearer {DEFAULT_TOKEN}" ) assert responses.calls[0].request.headers["X-Request-Id"] == request_id assert ( responses.calls[0].request.headers["Content-Type"] == "application/json; charset=utf-8" ) assert responses.calls[0].request.body == json.dumps(data) assert response == default_task_response @responses.activate def test_post_return_ok_when_no_response_body(): responses.add( responses.POST, DEFAULT_URL, status=204, ) result = post(Session(), DEFAULT_URL, DEFAULT_TOKEN) assert result is True @responses.activate def test_post_raise_for_status(): with pytest.raises(HTTPError): responses.add( responses.POST, DEFAULT_URL, status=500, ) post(Session(), DEFAULT_URL, DEFAULT_TOKEN) @responses.activate def test_delete_with_request_id(): request_id = "12345" responses.add( responses.DELETE, DEFAULT_URL, status=204, ) result = delete(Session(), DEFAULT_URL, DEFAULT_TOKEN, {"request_id": request_id}) assert len(responses.calls) == 1 assert responses.calls[0].request.url == DEFAULT_URL assert ( responses.calls[0].request.headers["Authorization"] == f"Bearer {DEFAULT_TOKEN}" ) assert responses.calls[0].request.headers["X-Request-Id"] == request_id assert result is True @responses.activate def test_delete_raise_for_status(): with pytest.raises(HTTPError): responses.add( responses.DELETE, DEFAULT_URL, status=500, ) delete(Session(), DEFAULT_URL, DEFAULT_TOKEN) todoist-api-python-2.1.6/tests/test_models.py000066400000000000000000000402671465465710500213570ustar00rootroot00000000000000from __future__ import annotations from tests.data.quick_add_responses import ( QUICK_ADD_RESPONSE_FULL, QUICK_ADD_RESPONSE_MINIMAL, ) from tests.data.test_defaults import ( DEFAULT_ATTACHMENT_RESPONSE, DEFAULT_COLLABORATOR_RESPONSE, DEFAULT_COMMENT_RESPONSE, DEFAULT_COMPLETED_ITEMS_RESPONSE, DEFAULT_DUE_RESPONSE, DEFAULT_DURATION_RESPONSE, DEFAULT_ITEM_COMPLETED_INFO_RESPONSE, DEFAULT_ITEM_RESPONSE, DEFAULT_LABEL_RESPONSE, DEFAULT_PROJECT_RESPONSE, DEFAULT_SECTION_RESPONSE, DEFAULT_TASK_RESPONSE, ) from todoist_api_python.models import ( Attachment, AuthResult, Collaborator, Comment, CompletedItems, Due, Duration, Item, ItemCompletedInfo, Label, Project, QuickAddResult, Section, Task, ) unexpected_data = {"unexpected_key": "some value"} def test_project_from_dict(): 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.color == sample_data["color"] assert project.comment_count == sample_data["comment_count"] assert project.is_favorite == sample_data["is_favorite"] assert project.name == sample_data["name"] assert project.is_shared == sample_data["is_shared"] assert project.url == sample_data["url"] assert project.is_inbox_project == sample_data["is_inbox_project"] assert project.is_team_inbox == sample_data["is_team_inbox"] assert project.order == sample_data["order"] assert project.parent_id == sample_data["parent_id"] assert project.view_style == sample_data["view_style"] assert project.can_assign_tasks == sample_data["can_assign_tasks"] def test_section_from_dict(): 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.name == sample_data["name"] assert section.order == sample_data["order"] assert section.project_id == sample_data["project_id"] def test_due_from_dict(): sample_data = dict(DEFAULT_DUE_RESPONSE) sample_data.update(unexpected_data) due = Due.from_dict(sample_data) assert due.date == sample_data["date"] assert due.is_recurring == sample_data["is_recurring"] assert due.string == sample_data["string"] assert due.datetime == sample_data["datetime"] assert due.timezone == sample_data["timezone"] def test_duration_from_dict(): 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_task_from_dict(): sample_data = dict(DEFAULT_TASK_RESPONSE) sample_data.update(unexpected_data) task = Task.from_dict(sample_data) assert task.comment_count == sample_data["comment_count"] assert task.is_completed == sample_data["is_completed"] assert task.content == sample_data["content"] assert task.created_at == sample_data["created_at"] assert task.creator_id == sample_data["creator_id"] assert task.id == sample_data["id"] assert task.project_id == sample_data["project_id"] assert task.section_id == sample_data["section_id"] assert task.priority == sample_data["priority"] assert task.url == sample_data["url"] assert task.assignee_id == sample_data["assignee_id"] assert task.assigner_id == sample_data["assigner_id"] assert task.due == Due.from_dict(sample_data["due"]) assert task.labels == sample_data["labels"] assert task.order == sample_data["order"] assert task.parent_id == sample_data["parent_id"] assert task.duration == Duration.from_dict(sample_data["duration"]) def test_task_to_dict(): sample_data = dict(DEFAULT_TASK_RESPONSE) sample_data.update(unexpected_data) task = Task.from_dict(sample_data).to_dict() assert task["comment_count"] == sample_data["comment_count"] assert task["is_completed"] == sample_data["is_completed"] assert task["content"] == sample_data["content"] assert task["created_at"] == sample_data["created_at"] assert task["creator_id"] == sample_data["creator_id"] assert task["id"] == sample_data["id"] assert task["project_id"] == sample_data["project_id"] assert task["section_id"] == sample_data["section_id"] assert task["priority"] == sample_data["priority"] assert task["url"] == sample_data["url"] assert task["assignee_id"] == sample_data["assignee_id"] assert task["assigner_id"] == sample_data["assigner_id"] for key in task["due"]: assert task["due"][key] == sample_data["due"][key] assert task["labels"] == sample_data["labels"] assert task["order"] == sample_data["order"] assert task["parent_id"] == sample_data["parent_id"] for key in task["duration"]: assert task["duration"][key] == sample_data["duration"][key] def test_collaborator_from_dict(): 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(): 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(): 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.posted_at == 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(): 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_quick_add_result_minimal(): sample_data = dict(QUICK_ADD_RESPONSE_MINIMAL) sample_data.update(unexpected_data) quick_add_result = QuickAddResult.from_quick_add_response(sample_data) assert quick_add_result.task.comment_count == 0 assert quick_add_result.task.is_completed is False assert quick_add_result.task.content == "some task" assert quick_add_result.task.created_at == "2021-02-05T11:02:56.00000Z" assert quick_add_result.task.creator_id == "21180723" assert quick_add_result.task.id == "4554989047" assert quick_add_result.task.project_id == "2203108698" assert quick_add_result.task.section_id is None assert quick_add_result.task.priority == 1 assert quick_add_result.task.url == "https://todoist.com/showTask?id=4554989047" assert quick_add_result.task.assignee_id is None assert quick_add_result.task.assigner_id is None assert quick_add_result.task.due is None assert quick_add_result.task.labels == [] assert quick_add_result.task.order == 6 assert quick_add_result.task.parent_id is None assert quick_add_result.task.sync_id is None assert quick_add_result.resolved_assignee_name is None assert quick_add_result.resolved_label_names == [] assert quick_add_result.resolved_project_name is None assert quick_add_result.resolved_section_name is None def test_quick_add_result_full(): sample_data = dict(QUICK_ADD_RESPONSE_FULL) sample_data.update(unexpected_data) quick_add_result = QuickAddResult.from_quick_add_response(sample_data) assert quick_add_result.task.comment_count == 0 assert quick_add_result.task.is_completed is False assert quick_add_result.task.content == "some task" assert quick_add_result.task.created_at == "2021-02-05T11:04:54.00000Z" assert quick_add_result.task.creator_id == "21180723" assert quick_add_result.task.id == "4554993687" assert quick_add_result.task.project_id == "2257514220" assert quick_add_result.task.section_id == "2232454220" assert quick_add_result.task.priority == 1 assert ( quick_add_result.task.url == "https://todoist.com/showTask?id=4554993687&sync_id=4554993687" ) assert quick_add_result.task.assignee_id == "29172386" assert quick_add_result.task.assigner_id == "21180723" assert quick_add_result.task.due.date == "2021-02-06T11:00:00.00000Z" assert quick_add_result.task.due.is_recurring is False assert quick_add_result.task.due.string == "Feb 6 11:00 AM" assert quick_add_result.task.due.datetime == "2021-02-06T11:00:00.00000Z" assert quick_add_result.task.due.timezone == "Europe/London" assert quick_add_result.task.labels == ["Label1", "Label2"] assert quick_add_result.task.order == 1 assert quick_add_result.task.parent_id is None assert quick_add_result.task.sync_id == "4554993687" assert quick_add_result.resolved_assignee_name == "Some Guy" assert quick_add_result.resolved_label_names == ["Label1", "Label2"] assert quick_add_result.resolved_project_name == "test" assert quick_add_result.resolved_section_name == "A section" def test_quick_add_broken_data(): none_attribute = QUICK_ADD_RESPONSE_FULL.copy() missing_attribute = QUICK_ADD_RESPONSE_FULL.copy() none_attribute["meta"]["project"] = None none_attribute["meta"]["assignee"] = None none_attribute["meta"]["section"] = None del missing_attribute["meta"]["project"] del missing_attribute["meta"]["assignee"] del missing_attribute["meta"]["section"] for quick_add_responses in [none_attribute, missing_attribute]: sample_data = dict(quick_add_responses) sample_data.update(unexpected_data) quick_add_result = QuickAddResult.from_quick_add_response(sample_data) assert quick_add_result.task.comment_count == 0 assert quick_add_result.task.is_completed is False assert quick_add_result.task.content == "some task" assert quick_add_result.task.created_at == "2021-02-05T11:04:54.00000Z" assert quick_add_result.task.creator_id == "21180723" assert quick_add_result.task.id == "4554993687" assert quick_add_result.task.project_id == "2257514220" assert quick_add_result.task.section_id == "2232454220" assert quick_add_result.task.priority == 1 assert ( quick_add_result.task.url == "https://todoist.com/showTask?id=4554993687&sync_id=4554993687" ) assert quick_add_result.task.assignee_id == "29172386" assert quick_add_result.task.assigner_id == "21180723" assert quick_add_result.task.due.date == "2021-02-06T11:00:00.00000Z" assert quick_add_result.task.due.is_recurring is False assert quick_add_result.task.due.string == "Feb 6 11:00 AM" assert quick_add_result.task.due.datetime == "2021-02-06T11:00:00.00000Z" assert quick_add_result.task.due.timezone == "Europe/London" assert quick_add_result.task.labels == ["Label1", "Label2"] assert quick_add_result.task.order == 1 assert quick_add_result.task.parent_id is None assert quick_add_result.task.sync_id == "4554993687" assert quick_add_result.resolved_assignee_name is None assert quick_add_result.resolved_label_names == ["Label1", "Label2"] assert quick_add_result.resolved_project_name is None assert quick_add_result.resolved_section_name is None def test_auth_result_from_dict(): 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 def test_item_from_dict(): sample_data = dict(DEFAULT_ITEM_RESPONSE) sample_data.update(unexpected_data) item = Item.from_dict(sample_data) assert item.id == "2995104339" assert item.user_id == "2671355" assert item.project_id == "2203306141" assert item.content == "Buy Milk" assert item.description == "" assert item.priority == 1 assert item.due.date == DEFAULT_DUE_RESPONSE["date"] assert item.due.is_recurring == DEFAULT_DUE_RESPONSE["is_recurring"] assert item.due.string == DEFAULT_DUE_RESPONSE["string"] assert item.due.datetime == DEFAULT_DUE_RESPONSE["datetime"] assert item.due.timezone == DEFAULT_DUE_RESPONSE["timezone"] assert item.parent_id is None assert item.child_order == 1 assert item.section_id is None assert item.day_order == -1 assert item.collapsed is False assert item.labels == ["Food", "Shopping"] assert item.added_by_uid == "2671355" assert item.assigned_by_uid == "2671355" assert item.responsible_uid is None assert item.checked is False assert item.is_deleted is False assert item.sync_id is None assert item.added_at == "2014-09-26T08:25:05.000000Z" def test_item_completed_info_from_dict(): sample_data = dict(DEFAULT_ITEM_COMPLETED_INFO_RESPONSE) sample_data.update(unexpected_data) info = ItemCompletedInfo.from_dict(sample_data) assert info.item_id == "2995104339" assert info.completed_items == 12 def test_completed_items_from_dict(): sample_data = dict(DEFAULT_COMPLETED_ITEMS_RESPONSE) sample_data.update(unexpected_data) completed_items = CompletedItems.from_dict(sample_data) assert completed_items.total == 22 assert completed_items.next_cursor == "k85gVI5ZAs8AAAABFoOzAQ" assert completed_items.has_more is True assert len(completed_items.items) == 1 assert completed_items.items[0].id == "2995104339" assert completed_items.items[0].user_id == "2671355" assert completed_items.items[0].project_id == "2203306141" assert completed_items.items[0].content == "Buy Milk" assert completed_items.items[0].description == "" assert completed_items.items[0].priority == 1 assert completed_items.items[0].due.date == DEFAULT_DUE_RESPONSE["date"] assert ( completed_items.items[0].due.is_recurring == DEFAULT_DUE_RESPONSE["is_recurring"] ) assert completed_items.items[0].due.string == DEFAULT_DUE_RESPONSE["string"] assert completed_items.items[0].due.datetime == DEFAULT_DUE_RESPONSE["datetime"] assert completed_items.items[0].due.timezone == DEFAULT_DUE_RESPONSE["timezone"] assert completed_items.items[0].parent_id is None assert completed_items.items[0].child_order == 1 assert completed_items.items[0].section_id is None assert completed_items.items[0].day_order == -1 assert completed_items.items[0].collapsed is False assert completed_items.items[0].labels == ["Food", "Shopping"] assert completed_items.items[0].added_by_uid == "2671355" assert completed_items.items[0].assigned_by_uid == "2671355" assert completed_items.items[0].responsible_uid is None assert completed_items.items[0].checked is False assert completed_items.items[0].is_deleted is False assert completed_items.items[0].sync_id is None assert completed_items.items[0].added_at == "2014-09-26T08:25:05.000000Z" assert len(completed_items.completed_info) == 1 assert completed_items.completed_info[0].item_id == "2995104339" assert completed_items.completed_info[0].completed_items == 12 todoist-api-python-2.1.6/tests/utils/000077500000000000000000000000001465465710500176125ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/utils/__init__.py000066400000000000000000000000001465465710500217110ustar00rootroot00000000000000todoist-api-python-2.1.6/tests/utils/test_utils.py000066400000000000000000000013211465465710500223600ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING from tests.data.test_defaults import DEFAULT_REQUEST_ID, DEFAULT_TOKEN from todoist_api_python.api import TodoistAPI if TYPE_CHECKING: from collections.abc import Callable MATCH_ANY_REGEX = re.compile(".*") def assert_auth_header(request): assert request.headers["Authorization"] == f"Bearer {DEFAULT_TOKEN}" def assert_request_id_header(request): assert request.headers["X-Request-Id"] == DEFAULT_REQUEST_ID def get_todoist_api_patch(method: Callable | None) -> str: module = TodoistAPI.__module__ name = TodoistAPI.__qualname__ return f"{module}.{name}.{method.__name__}" if method else f"{module}.{name}" todoist-api-python-2.1.6/todoist_api_python/000077500000000000000000000000001465465710500212275ustar00rootroot00000000000000todoist-api-python-2.1.6/todoist_api_python/__init__.py000066400000000000000000000000001465465710500233260ustar00rootroot00000000000000todoist-api-python-2.1.6/todoist_api_python/api.py000066400000000000000000000226021465465710500223540ustar00rootroot00000000000000from __future__ import annotations from typing import Any from weakref import finalize import requests from todoist_api_python.endpoints import ( COLLABORATORS_ENDPOINT, COMMENTS_ENDPOINT, COMPLETED_ITEMS_ENDPOINT, LABELS_ENDPOINT, PROJECTS_ENDPOINT, QUICK_ADD_ENDPOINT, SECTIONS_ENDPOINT, SHARED_LABELS_ENDPOINT, SHARED_LABELS_REMOVE_ENDPOINT, SHARED_LABELS_RENAME_ENDPOINT, TASKS_ENDPOINT, get_rest_url, get_sync_url, ) from todoist_api_python.http_requests import delete, get, post from todoist_api_python.models import ( Collaborator, Comment, CompletedItems, Label, Project, QuickAddResult, Section, Task, ) class TodoistAPI: def __init__(self, token: str, session: requests.Session | None = None) -> None: self._token: str = token self._session = session or requests.Session() self._finalizer = finalize(self, self._session.close) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self._finalizer() def get_task(self, task_id: str) -> Task: endpoint = get_rest_url(f"{TASKS_ENDPOINT}/{task_id}") task = get(self._session, endpoint, self._token) return Task.from_dict(task) def get_tasks(self, **kwargs) -> list[Task]: ids = kwargs.pop("ids", None) if ids: kwargs.update({"ids": ",".join(str(i) for i in ids)}) endpoint = get_rest_url(TASKS_ENDPOINT) tasks = get(self._session, endpoint, self._token, kwargs) return [Task.from_dict(obj) for obj in tasks] def add_task(self, content: str, **kwargs) -> Task: endpoint = get_rest_url(TASKS_ENDPOINT) data: dict[str, Any] = {"content": content} data.update(kwargs) task = post(self._session, endpoint, self._token, data=data) return Task.from_dict(task) def update_task(self, task_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{TASKS_ENDPOINT}/{task_id}") return post(self._session, endpoint, self._token, data=kwargs) def close_task(self, task_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{TASKS_ENDPOINT}/{task_id}/close") return post(self._session, endpoint, self._token, data=kwargs) def reopen_task(self, task_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{TASKS_ENDPOINT}/{task_id}/reopen") return post(self._session, endpoint, self._token, data=kwargs) def delete_task(self, task_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{TASKS_ENDPOINT}/{task_id}") return delete(self._session, endpoint, self._token, args=kwargs) def quick_add_task(self, text: str) -> QuickAddResult: endpoint = get_sync_url(QUICK_ADD_ENDPOINT) data = { "text": text, "meta": True, "auto_reminder": True, } task_data = post(self._session, endpoint, self._token, data=data) return QuickAddResult.from_quick_add_response(task_data) def get_project(self, project_id: str) -> Project: endpoint = get_rest_url(f"{PROJECTS_ENDPOINT}/{project_id}") project = get(self._session, endpoint, self._token) return Project.from_dict(project) def get_projects(self) -> list[Project]: endpoint = get_rest_url(PROJECTS_ENDPOINT) projects = get(self._session, endpoint, self._token) return [Project.from_dict(obj) for obj in projects] def add_project(self, name: str, **kwargs) -> Project: endpoint = get_rest_url(PROJECTS_ENDPOINT) data: dict[str, Any] = {"name": name} data.update(kwargs) project = post(self._session, endpoint, self._token, data=data) return Project.from_dict(project) def update_project(self, project_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{PROJECTS_ENDPOINT}/{project_id}") return post(self._session, endpoint, self._token, data=kwargs) def delete_project(self, project_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{PROJECTS_ENDPOINT}/{project_id}") return delete(self._session, endpoint, self._token, args=kwargs) def get_collaborators(self, project_id: str) -> list[Collaborator]: endpoint = get_rest_url( f"{PROJECTS_ENDPOINT}/{project_id}/{COLLABORATORS_ENDPOINT}" ) collaborators = get(self._session, endpoint, self._token) return [Collaborator.from_dict(obj) for obj in collaborators] def get_section(self, section_id: str) -> Section: endpoint = get_rest_url(f"{SECTIONS_ENDPOINT}/{section_id}") section = get(self._session, endpoint, self._token) return Section.from_dict(section) def get_sections(self, **kwargs) -> list[Section]: endpoint = get_rest_url(SECTIONS_ENDPOINT) sections = get(self._session, endpoint, self._token, kwargs) return [Section.from_dict(obj) for obj in sections] def add_section(self, name: str, project_id: str, **kwargs) -> Section: endpoint = get_rest_url(SECTIONS_ENDPOINT) data = {"name": name, "project_id": project_id} data.update(kwargs) section = post(self._session, endpoint, self._token, data=data) return Section.from_dict(section) def update_section(self, section_id: str, name: str, **kwargs) -> bool: endpoint = get_rest_url(f"{SECTIONS_ENDPOINT}/{section_id}") data: dict[str, Any] = {"name": name} data.update(kwargs) return post(self._session, endpoint, self._token, data=data) def delete_section(self, section_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{SECTIONS_ENDPOINT}/{section_id}") return delete(self._session, endpoint, self._token, args=kwargs) def get_comment(self, comment_id: str) -> Comment: endpoint = get_rest_url(f"{COMMENTS_ENDPOINT}/{comment_id}") comment = get(self._session, endpoint, self._token) return Comment.from_dict(comment) def get_comments(self, **kwargs) -> list[Comment]: endpoint = get_rest_url(COMMENTS_ENDPOINT) comments = get(self._session, endpoint, self._token, kwargs) return [Comment.from_dict(obj) for obj in comments] def add_comment(self, content: str, **kwargs) -> Comment: endpoint = get_rest_url(COMMENTS_ENDPOINT) data = {"content": content} data.update(kwargs) comment = post(self._session, endpoint, self._token, data=data) return Comment.from_dict(comment) def update_comment(self, comment_id: str, content: str, **kwargs) -> bool: endpoint = get_rest_url(f"{COMMENTS_ENDPOINT}/{comment_id}") data: dict[str, Any] = {"content": content} data.update(kwargs) return post(self._session, endpoint, self._token, data=data) def delete_comment(self, comment_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{COMMENTS_ENDPOINT}/{comment_id}") return delete(self._session, endpoint, self._token, args=kwargs) def get_label(self, label_id: str) -> Label: endpoint = get_rest_url(f"{LABELS_ENDPOINT}/{label_id}") label = get(self._session, endpoint, self._token) return Label.from_dict(label) def get_labels(self) -> list[Label]: endpoint = get_rest_url(LABELS_ENDPOINT) labels = get(self._session, endpoint, self._token) return [Label.from_dict(obj) for obj in labels] def add_label(self, name: str, **kwargs) -> Label: endpoint = get_rest_url(LABELS_ENDPOINT) data = {"name": name} data.update(kwargs) label = post(self._session, endpoint, self._token, data=data) return Label.from_dict(label) def update_label(self, label_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{LABELS_ENDPOINT}/{label_id}") return post(self._session, endpoint, self._token, data=kwargs) def delete_label(self, label_id: str, **kwargs) -> bool: endpoint = get_rest_url(f"{LABELS_ENDPOINT}/{label_id}") return delete(self._session, endpoint, self._token, args=kwargs) def get_shared_labels(self) -> list[str]: endpoint = get_rest_url(SHARED_LABELS_ENDPOINT) return get(self._session, endpoint, self._token) def rename_shared_label(self, name: str, new_name: str) -> bool: endpoint = get_rest_url(SHARED_LABELS_RENAME_ENDPOINT) data = {"name": name, "new_name": new_name} return post(self._session, endpoint, self._token, data=data) def remove_shared_label(self, name: str) -> bool: endpoint = get_rest_url(SHARED_LABELS_REMOVE_ENDPOINT) data = {"name": name} return post(self._session, endpoint, self._token, data=data) def get_completed_items( self, project_id: str | None = None, section_id: str | None = None, item_id: str | None = None, last_seen_id: str | None = None, limit: int | None = None, cursor: str | None = None, ) -> CompletedItems: endpoint = get_sync_url(COMPLETED_ITEMS_ENDPOINT) completed_items = get( self._session, endpoint, self._token, { "project_id": project_id, "section_id": section_id, "item_id": item_id, "last_seen_id": last_seen_id, "limit": limit, "cursor": cursor, }, ) return CompletedItems.from_dict(completed_items) todoist-api-python-2.1.6/todoist_api_python/api_async.py000066400000000000000000000131461465465710500235540ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from todoist_api_python.api import TodoistAPI from todoist_api_python.utils import run_async if TYPE_CHECKING: import requests from todoist_api_python.models import ( Collaborator, Comment, CompletedItems, Label, Project, QuickAddResult, Section, Task, ) class TodoistAPIAsync: def __init__(self, token: str, session: requests.Session | None = None) -> None: self._api = TodoistAPI(token, session) async def get_task(self, task_id: str) -> Task: return await run_async(lambda: self._api.get_task(task_id)) async def get_tasks(self, **kwargs) -> list[Task]: return await run_async(lambda: self._api.get_tasks(**kwargs)) async def add_task(self, content: str, **kwargs) -> Task: return await run_async(lambda: self._api.add_task(content, **kwargs)) async def update_task(self, task_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.update_task(task_id, **kwargs)) async def close_task(self, task_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.close_task(task_id, **kwargs)) async def reopen_task(self, task_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.reopen_task(task_id, **kwargs)) async def delete_task(self, task_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.delete_task(task_id, **kwargs)) async def quick_add_task(self, text: str) -> QuickAddResult: return await run_async(lambda: self._api.quick_add_task(text)) async def get_project(self, project_id: str) -> Project: return await run_async(lambda: self._api.get_project(project_id)) async def get_projects(self) -> list[Project]: return await run_async(lambda: self._api.get_projects()) async def add_project(self, name: str, **kwargs) -> Project: return await run_async(lambda: self._api.add_project(name, **kwargs)) async def update_project(self, project_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.update_project(project_id, **kwargs)) async def delete_project(self, project_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.delete_project(project_id, **kwargs)) async def get_collaborators(self, project_id: str) -> list[Collaborator]: return await run_async(lambda: self._api.get_collaborators(project_id)) async def get_section(self, section_id: str) -> Section: return await run_async(lambda: self._api.get_section(section_id)) async def get_sections(self, **kwargs) -> list[Section]: return await run_async(lambda: self._api.get_sections(**kwargs)) async def add_section(self, name: str, project_id: str, **kwargs) -> Section: return await run_async( lambda: self._api.add_section(name, project_id, **kwargs) ) async def update_section(self, section_id: str, name: str, **kwargs) -> bool: return await run_async( lambda: self._api.update_section(section_id, name, **kwargs) ) async def delete_section(self, section_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.delete_section(section_id, **kwargs)) async def get_comment(self, comment_id: str) -> Comment: return await run_async(lambda: self._api.get_comment(comment_id)) async def get_comments(self, **kwargs) -> list[Comment]: return await run_async(lambda: self._api.get_comments(**kwargs)) async def add_comment(self, content: str, **kwargs) -> Comment: return await run_async(lambda: self._api.add_comment(content, **kwargs)) async def update_comment(self, comment_id: str, content: str, **kwargs) -> bool: return await run_async( lambda: self._api.update_comment(comment_id, content, **kwargs) ) async def delete_comment(self, comment_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.delete_comment(comment_id, **kwargs)) async def get_label(self, label_id: str) -> Label: return await run_async(lambda: self._api.get_label(label_id)) async def get_labels(self) -> list[Label]: return await run_async(lambda: self._api.get_labels()) async def add_label(self, name: str, **kwargs) -> Label: return await run_async(lambda: self._api.add_label(name, **kwargs)) async def update_label(self, label_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.update_label(label_id, **kwargs)) async def delete_label(self, label_id: str, **kwargs) -> bool: return await run_async(lambda: self._api.delete_label(label_id, **kwargs)) async def get_shared_labels(self) -> list[str]: return await run_async(lambda: self._api.get_shared_labels()) async def rename_shared_label(self, name: str, new_name: str) -> bool: return await run_async(lambda: self._api.rename_shared_label(name, new_name)) async def remove_shared_label(self, name: str) -> bool: return await run_async(lambda: self._api.remove_shared_label(name)) async def get_completed_items( self, project_id: str | None = None, section_id: str | None = None, item_id: str | None = None, last_seen_id: str | None = None, limit: int | None = None, cursor: str | None = None, ) -> CompletedItems: return await run_async( lambda: self._api.get_completed_items( project_id, section_id, item_id, last_seen_id, limit, cursor ) ) todoist-api-python-2.1.6/todoist_api_python/authentication.py000066400000000000000000000037511465465710500246260ustar00rootroot00000000000000from __future__ import annotations from urllib.parse import urlencode import requests from requests import Session from todoist_api_python.endpoints import ( AUTHORIZE_ENDPOINT, REVOKE_TOKEN_ENDPOINT, TOKEN_ENDPOINT, get_auth_url, get_sync_url, ) from todoist_api_python.http_requests import post from todoist_api_python.models import AuthResult from todoist_api_python.utils import run_async def get_auth_token( client_id: str, client_secret: str, code: str, session: Session | None = None ) -> AuthResult: endpoint = get_auth_url(TOKEN_ENDPOINT) session = session or requests.Session() payload = {"client_id": client_id, "client_secret": client_secret, "code": code} response = post(session=session, url=endpoint, data=payload) 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: endpoint = get_sync_url(REVOKE_TOKEN_ENDPOINT) session = session or requests.Session() payload = { "client_id": client_id, "client_secret": client_secret, "access_token": token, } response = post(session=session, url=endpoint, data=payload) return response 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)) class ArgumentError(Exception): pass def get_authentication_url(client_id: str, scopes: list[str], state: str) -> str: if len(scopes) == 0: raise ArgumentError("At least one authorization scope should be requested.") query = {"client_id": client_id, "scope": ",".join(scopes), "state": state} auth_url = get_auth_url(AUTHORIZE_ENDPOINT) return f"{auth_url}?{urlencode(query)}" todoist-api-python-2.1.6/todoist_api_python/endpoints.py000066400000000000000000000021631465465710500236060ustar00rootroot00000000000000from __future__ import annotations from urllib.parse import urljoin BASE_URL = "https://api.todoist.com" AUTH_BASE_URL = "https://todoist.com" SYNC_VERSION = "v9" REST_VERSION = "v2" SYNC_API = urljoin(BASE_URL, f"/sync/{SYNC_VERSION}/") REST_API = urljoin(BASE_URL, f"/rest/{REST_VERSION}/") TASKS_ENDPOINT = "tasks" PROJECTS_ENDPOINT = "projects" COLLABORATORS_ENDPOINT = "collaborators" SECTIONS_ENDPOINT = "sections" COMMENTS_ENDPOINT = "comments" LABELS_ENDPOINT = "labels" SHARED_LABELS_ENDPOINT = "labels/shared" SHARED_LABELS_RENAME_ENDPOINT = f"{SHARED_LABELS_ENDPOINT}/rename" SHARED_LABELS_REMOVE_ENDPOINT = f"{SHARED_LABELS_ENDPOINT}/remove" QUICK_ADD_ENDPOINT = "quick/add" AUTHORIZE_ENDPOINT = "oauth/authorize" TOKEN_ENDPOINT = "oauth/access_token" REVOKE_TOKEN_ENDPOINT = "access_tokens/revoke" COMPLETED_ITEMS_ENDPOINT = "archive/items" def get_rest_url(relative_path: str) -> str: return urljoin(REST_API, relative_path) def get_sync_url(relative_path: str) -> str: return urljoin(SYNC_API, relative_path) def get_auth_url(relative_path: str) -> str: return urljoin(AUTH_BASE_URL, relative_path) todoist-api-python-2.1.6/todoist_api_python/headers.py000066400000000000000000000011541465465710500232150ustar00rootroot00000000000000from __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-2.1.6/todoist_api_python/http_requests.py000066400000000000000000000026451465465710500245220ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any from todoist_api_python.headers import create_headers if TYPE_CHECKING: from requests import Session def get( session: Session, url: str, token: str | None = None, params: dict[str, Any] | None = None, ): response = session.get(url, params=params, headers=create_headers(token=token)) if response.status_code == 200: return response.json() response.raise_for_status() return response.ok def post( session: Session, url: str, token: str | None = None, data: dict[str, Any] | None = None, ): request_id = data.pop("request_id", None) if data else None 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, ) if response.status_code == 200: return response.json() response.raise_for_status() return response.ok def delete( session: Session, url: str, token: str | None = None, args: dict[str, Any] | None = None, ): request_id = args.pop("request_id", None) if args else None headers = create_headers(token=token, request_id=request_id) response = session.delete( url, headers=headers, ) response.raise_for_status() return response.ok todoist-api-python-2.1.6/todoist_api_python/models.py000066400000000000000000000273351465465710500230760ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, fields from typing import Any, Literal from todoist_api_python.utils import get_url_for_task VIEW_STYLE = Literal["list", "board"] @dataclass class Project: color: str comment_count: int id: str is_favorite: bool is_inbox_project: bool is_shared: bool is_team_inbox: bool can_assign_tasks: bool name: str order: int parent_id: str | None url: str view_style: VIEW_STYLE @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( color=obj["color"], comment_count=obj["comment_count"], id=obj["id"], is_favorite=obj["is_favorite"], is_inbox_project=obj["is_inbox_project"], is_shared=obj["is_shared"], is_team_inbox=obj["is_team_inbox"], can_assign_tasks=obj["can_assign_tasks"], name=obj["name"], order=obj["order"], parent_id=obj.get("parent_id"), url=obj["url"], view_style=obj["view_style"], ) @dataclass class Section: id: str name: str order: int project_id: str @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( id=obj["id"], name=obj["name"], order=obj["order"], project_id=obj["project_id"], ) @dataclass class Due: date: str is_recurring: bool string: str datetime: str | None = None timezone: str | None = None @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( date=obj["date"], is_recurring=obj["is_recurring"], string=obj["string"], datetime=obj.get("datetime"), timezone=obj.get("timezone"), ) def to_dict(self) -> dict[str, Any]: return { "date": self.date, "is_recurring": self.is_recurring, "string": self.string, "datetime": self.datetime, "timezone": self.timezone, } @classmethod def from_quick_add_response(cls, obj: dict[str, Any]): due = obj.get("due") if not due: return None timezone = due.get("timezone") datetime: str | None = None if timezone: datetime = due["date"] return cls( date=due["date"], is_recurring=due["is_recurring"], string=due["string"], datetime=datetime, timezone=timezone, ) @dataclass class Task: assignee_id: str | None assigner_id: str | None comment_count: int is_completed: bool content: str created_at: str creator_id: str description: str due: Due | None id: str labels: list[str] | None order: int parent_id: str | None priority: int project_id: str section_id: str | None url: str duration: Duration | None sync_id: str | None = None @classmethod def from_dict(cls, obj: dict[str, Any]): due: Due | None = None duration: Duration | None = None if obj.get("due"): due = Due.from_dict(obj["due"]) if obj.get("duration"): duration = Duration.from_dict(obj["duration"]) return cls( assignee_id=obj.get("assignee_id"), assigner_id=obj.get("assigner_id"), comment_count=obj["comment_count"], is_completed=obj["is_completed"], content=obj["content"], created_at=obj["created_at"], creator_id=obj["creator_id"], description=obj["description"], due=due, id=obj["id"], labels=obj.get("labels"), order=obj["order"], parent_id=obj.get("parent_id"), priority=obj["priority"], project_id=obj["project_id"], section_id=obj.get("section_id"), url=obj["url"], duration=duration, ) def to_dict(self) -> dict[str, Any]: due: dict[str, Any] | None = None duration: dict[str, Any] | None = None if self.due: due = self.due.to_dict() if self.duration: duration = self.duration.to_dict() return { "assignee_id": self.assignee_id, "assigner_id": self.assigner_id, "comment_count": self.comment_count, "is_completed": self.is_completed, "content": self.content, "created_at": self.created_at, "creator_id": self.creator_id, "description": self.description, "due": due, "id": self.id, "labels": self.labels, "order": self.order, "parent_id": self.parent_id, "priority": self.priority, "project_id": self.project_id, "section_id": self.section_id, "sync_id": self.sync_id, "url": self.url, "duration": duration, } @classmethod def from_quick_add_response(cls, obj: dict[str, Any]): due: Due | None = None duration: Duration | None = None if obj.get("due"): due = Due.from_quick_add_response(obj) if obj.get("duration"): duration = Duration.from_dict(obj["duration"]) return cls( assignee_id=obj.get("responsible_uid"), assigner_id=obj.get("assigned_by_uid"), comment_count=0, is_completed=False, content=obj["content"], created_at=obj["added_at"], creator_id=obj["added_by_uid"], description=obj["description"], due=due, duration=duration, id=obj["id"], labels=obj["labels"], order=obj["child_order"], parent_id=obj["parent_id"] or None, priority=obj["priority"], project_id=obj["project_id"], section_id=obj["section_id"] or None, sync_id=obj["sync_id"], url=get_url_for_task(obj["id"], obj["sync_id"]), ) @dataclass class QuickAddResult: task: Task resolved_project_name: str | None = None resolved_assignee_name: str | None = None resolved_label_names: list[str] | None = None resolved_section_name: str | None = None @classmethod def from_quick_add_response(cls, obj: dict[str, Any]): project_data = obj["meta"].get("project", {}) assignee_data = obj["meta"].get("assignee", {}) section_data = obj["meta"].get("section", {}) resolved_project_name = None resolved_assignee_name = None resolved_section_name = None if project_data and len(project_data) == 2: resolved_project_name = obj["meta"]["project"][1] if assignee_data and len(assignee_data) == 2: resolved_assignee_name = obj["meta"]["assignee"][1] if section_data and len(section_data) == 2: resolved_section_name = obj["meta"]["section"][1] return cls( task=Task.from_quick_add_response(obj), resolved_project_name=resolved_project_name, resolved_assignee_name=resolved_assignee_name, resolved_label_names=list(obj["meta"]["labels"].values()), resolved_section_name=resolved_section_name, ) @dataclass class Collaborator: id: str email: str name: str @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( id=obj["id"], email=obj["email"], name=obj["name"], ) @dataclass class Attachment: 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 @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( resource_type=obj.get("resource_type"), file_name=obj.get("file_name"), file_size=obj.get("file_size"), file_type=obj.get("file_type"), file_url=obj.get("file_url"), upload_state=obj.get("upload_state"), image=obj.get("image"), image_width=obj.get("image_width"), image_height=obj.get("image_height"), url=obj.get("url"), title=obj.get("title"), ) @dataclass class Comment: attachment: Attachment | None content: str id: str posted_at: str project_id: str | None task_id: str | None @classmethod def from_dict(cls, obj: dict[str, Any]): attachment: Attachment | None = None if "attachment" in obj and obj["attachment"] is not None: attachment = Attachment.from_dict(obj["attachment"]) return cls( attachment=attachment, content=obj["content"], id=obj["id"], posted_at=obj["posted_at"], project_id=obj.get("project_id"), task_id=obj.get("task_id"), ) @dataclass class Label: id: str name: str color: str order: int is_favorite: bool @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( id=obj["id"], name=obj["name"], color=obj["color"], order=obj["order"], is_favorite=obj["is_favorite"], ) @dataclass class AuthResult: access_token: str state: str | None @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( access_token=obj["access_token"], state=obj.get("state"), ) @dataclass class Item: id: str user_id: str project_id: str content: str description: str priority: int child_order: int collapsed: bool labels: list[str] checked: bool is_deleted: bool added_at: str due: Due | None = None parent_id: int | None = None section_id: str | None = None day_order: int | None = None added_by_uid: str | None = None assigned_by_uid: str | None = None responsible_uid: str | None = None sync_id: str | None = None completed_at: str | None = None @classmethod def from_dict(cls, obj: dict[str, Any]) -> Item: params = {f.name: obj[f.name] for f in fields(cls) if f.name in obj} if (due := obj.get("due")) is not None: params["due"] = Due.from_dict(due) return cls(**params) @dataclass class ItemCompletedInfo: item_id: str completed_items: int @classmethod def from_dict(cls, obj: dict[str, Any]) -> ItemCompletedInfo: return cls(**{f.name: obj[f.name] for f in fields(cls)}) @dataclass class CompletedItems: items: list[Item] total: int completed_info: list[ItemCompletedInfo] has_more: bool next_cursor: str | None = None @classmethod def from_dict(cls, obj: dict[str, Any]) -> CompletedItems: return cls( items=[Item.from_dict(v) for v in obj["items"]], total=obj["total"], completed_info=[ ItemCompletedInfo.from_dict(v) for v in obj["completed_info"] ], has_more=obj["has_more"], next_cursor=obj.get("next_cursor"), ) @dataclass class Duration: amount: int unit: str @classmethod def from_dict(cls, obj: dict[str, Any]): return cls( amount=obj["amount"], unit=obj["unit"], ) def to_dict(self) -> dict[str, Any]: return { "amount": self.amount, "unit": self.unit, } todoist-api-python-2.1.6/todoist_api_python/py.typed000066400000000000000000000000001465465710500227140ustar00rootroot00000000000000todoist-api-python-2.1.6/todoist_api_python/utils.py000066400000000000000000000006641465465710500227470ustar00rootroot00000000000000from __future__ import annotations import asyncio SHOW_TASK_ENDPOINT = "https://todoist.com/showTask" def get_url_for_task(task_id: int, sync_id: int | None) -> str: return ( f"{SHOW_TASK_ENDPOINT}?id={task_id}&sync_id={sync_id}" if sync_id else f"{SHOW_TASK_ENDPOINT}?id={task_id}" ) async def run_async(func): loop = asyncio.get_event_loop() return await loop.run_in_executor(None, func) todoist-api-python-2.1.6/tox.ini000066400000000000000000000005721465465710500166270ustar00rootroot00000000000000[tox] envlist = py38, mypy [gh-actions] python = 3.8: py38, mypy [testenv] whitelist_externals = poetry skip_install = true commands = poetry install -v poetry run pytest {posargs} [testenv:mypy] whitelist_externals = poetry skip_install = true deps = mypy commands = poetry install -v poetry mypy aiohttp_todoist {posargs:--ignore-missing-imports}