diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2516d244..0a790ae7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,6 +73,15 @@ repos: - id: pyright - repo: local hooks: + # Remove this once ty has proper pre-commit support + # https://github.com/astral-sh/ty/issues/269 + - id: ty + name: ty + entry: uv run ty check + language: system + types: [python] + require_serial: true + pass_filenames: false - id: mypy name: mypy entry: uv run --no-active mypy diff --git a/pyproject.toml b/pyproject.toml index 74281383..6c89c2dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dev = [ ] mypy = [ "mypy>=1.15.0", + "ty>=0.0.11", "types-icalendar>=6.1.3.20250403", "types-python-dateutil>=2.9.0.20241206", ] diff --git a/scripts/checks/pypi_version.py b/scripts/checks/pypi_version.py index ed17d46c..5ad123a2 100755 --- a/scripts/checks/pypi_version.py +++ b/scripts/checks/pypi_version.py @@ -8,11 +8,23 @@ """ import argparse +import platform +import sys from pathlib import Path -import tomllib from packaging.version import InvalidVersion, parse +if sys.version_info >= (3, 11): + import tomllib +else: + msg = ( + "This script relies on the standard-library 'tomllib' module, which is only " + f"available starting with Python 3.11 (current: {platform.python_version()}). " + "Please run this script with Python 3.11 or newer (for example via a Python " + "3.11+ virtual environment or by configuring your tooling to use Python 3.11+)." + ) + raise ImportError(msg) + def parse_args() -> argparse.Namespace: """Parse command line arguments. diff --git a/scripts/generate_v1_token.py b/scripts/generate_v1_token.py index 3c66936c..b48e0990 100755 --- a/scripts/generate_v1_token.py +++ b/scripts/generate_v1_token.py @@ -38,7 +38,7 @@ def main() -> None: if confirm("Do you want the token to be copied to the clipboard?", default=True): # https://github.com/asweigart/pyperclip/issues/210 - import pyperclip # type: ignore[import-untyped] # noqa: PLC0415 + import pyperclip # type: ignore[import-untyped] # noqa: PLC0415 # ty: ignore[unused-ignore-comment] pyperclip.copy(str(token_value)) logger.info("Token copied to clipboard.") diff --git a/src/pyticktick/logger.py b/src/pyticktick/logger.py index b3389b9c..a0e2c4bf 100644 --- a/src/pyticktick/logger.py +++ b/src/pyticktick/logger.py @@ -40,7 +40,7 @@ def emit(self, record: logging.LogRecord) -> None: # noqa: D102 # Find caller from where originated the logged message. frame, depth = sys._getframe(6), 6 # noqa: SLF001 while frame and frame.f_code.co_filename == logging.__file__: - frame = frame.f_back # type: ignore[assignment] + frame = frame.f_back # type: ignore[assignment] # ty: ignore[unused-ignore-comment] depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( diff --git a/src/pyticktick/pydantic.py b/src/pyticktick/pydantic.py index 73096103..5d07f3f3 100644 --- a/src/pyticktick/pydantic.py +++ b/src/pyticktick/pydantic.py @@ -94,5 +94,7 @@ def update_model_config(model: type[BaseModel], **config_kwargs: Any) -> None: for field in model.__pydantic_fields__.values(): _check_field_for_submodel(field.annotation, **config_kwargs) - model.model_config.update(ConfigDict(**config_kwargs)) # type: ignore[typeddict-item] + model.model_config.update( + ConfigDict(**config_kwargs) # type: ignore[typeddict-item] # ty: ignore[unused-ignore-comment] + ) model.model_rebuild(force=True) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 21b2896b..67cf158b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -15,4 +15,4 @@ def client() -> Client: ) if not (env_file_path := Path(__file__).parents[2].joinpath(".env")).exists(): return Client() - return Client(_env_file=env_file_path) # pyright: ignore[reportCallIssue] # https://github.com/pydantic/pydantic/issues/3072 + return Client(_env_file=env_file_path) # pyright: ignore[reportCallIssue] # ty: ignore[unknown-argument] # https://github.com/pydantic/pydantic/issues/3072 diff --git a/tests/integration/v2/test_batch.py b/tests/integration/v2/test_batch.py index 19ae2c48..521b70bf 100644 --- a/tests/integration/v2/test_batch.py +++ b/tests/integration/v2/test_batch.py @@ -328,7 +328,7 @@ def test_get_batch_v2( # noqa: PLR0912, PLR0915 assert data["title"] == t.title assert data["project_id"] == t.project_id if "tags" in data: - assert set(data["tags"]) == set(t.tags) + assert set(data["tags"]) == set(t.tags) # ty: ignore[invalid-argument-type] if "desc" in data: assert data["desc"] == t.desc if "content" in data: @@ -360,34 +360,34 @@ def test_get_batch_v2( # noqa: PLR0912, PLR0915 if "kind" in data: assert data["kind"] == t.kind if "items" in data: - assert len(data["items"]) == len(t.items) - for item_data in data["items"]: + assert len(data["items"]) == len(t.items) # ty: ignore[invalid-argument-type] + for item_data in data["items"]: # ty: ignore[not-iterable] i = None - assert any(item_data["id"] == i.id for i in t.items) + assert any(item_data["id"] == i.id for i in t.items) # ty: ignore[invalid-argument-type] for i in t.items: - if i.id == item_data["id"]: + if i.id == item_data["id"]: # ty: ignore[invalid-argument-type] break assert isinstance(i, ItemV2) - assert item_data["id"] == i.id - assert item_data["title"] == i.title + assert item_data["id"] == i.id # ty: ignore[invalid-argument-type] + assert item_data["title"] == i.title # ty: ignore[invalid-argument-type] if "is_all_day" in item_data and data["is_all_day"] is None: - assert item_data["is_all_day"] == i.is_all_day + assert item_data["is_all_day"] == i.is_all_day # ty: ignore[invalid-argument-type] if "time_zone" in item_data and data["time_zone"] is None: - assert item_data["time_zone"] == i.time_zone + assert item_data["time_zone"] == i.time_zone # ty: ignore[invalid-argument-type] if "status" in item_data: - assert item_data["status"] == i.status + assert item_data["status"] == i.status # ty: ignore[invalid-argument-type] if "reminders" in data: assert isinstance(t.reminders, list) - assert len(data["reminders"]) == len(t.reminders) - for reminder_data in data["reminders"]: + assert len(data["reminders"]) == len(t.reminders) # ty: ignore[invalid-argument-type] + for reminder_data in data["reminders"]: # ty: ignore[not-iterable] r = None - assert any(reminder_data["id"] == r.id for r in t.reminders) + assert any(reminder_data["id"] == r.id for r in t.reminders) # ty: ignore[invalid-argument-type] for r in t.reminders: - if r.id == reminder_data["id"]: + if r.id == reminder_data["id"]: # ty: ignore[invalid-argument-type] break assert isinstance(r, TaskReminderV2) - assert reminder_data["id"] == r.id - assert reminder_data["trigger"] == r.trigger + assert reminder_data["id"] == r.id # ty: ignore[invalid-argument-type] + assert reminder_data["trigger"] == r.trigger # ty: ignore[invalid-argument-type] if t.reminder is not None: assert any(t.reminder == r.trigger for r in t.reminders) diff --git a/tests/integration/v2/test_closed.py b/tests/integration/v2/test_closed.py index eddb71b7..cf57233e 100644 --- a/tests/integration/v2/test_closed.py +++ b/tests/integration/v2/test_closed.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime +from datetime import datetime, timezone import pytest from pydantic import TypeAdapter @@ -166,7 +166,7 @@ def test_get_project_all_completed_v2(generate_object_id, delete_projects, clien assert data["title"] == t.title assert data["project_id"] == t.project_id if "tags" in data: - assert set(data["tags"]) == set(t.tags) + assert set(data["tags"]) == set(t.tags) # ty: ignore[invalid-argument-type] if "desc" in data: assert data["desc"] == t.desc if "content" in data: @@ -198,34 +198,34 @@ def test_get_project_all_completed_v2(generate_object_id, delete_projects, clien if "kind" in data: assert data["kind"] == t.kind if "items" in data: - assert len(data["items"]) == len(t.items) + assert len(data["items"]) == len(t.items) # ty: ignore[invalid-argument-type] i = None - for item_data in data["items"]: - assert any(item_data["id"] == i.id for i in t.items) + for item_data in data["items"]: # ty: ignore[not-iterable] + assert any(item_data["id"] == i.id for i in t.items) # ty: ignore[invalid-argument-type] for i in t.items: - if i.id == item_data["id"]: + if i.id == item_data["id"]: # ty: ignore[invalid-argument-type] break assert isinstance(i, ItemV2) - assert item_data["id"] == i.id - assert item_data["title"] == i.title + assert item_data["id"] == i.id # ty: ignore[invalid-argument-type] + assert item_data["title"] == i.title # ty: ignore[invalid-argument-type] if "is_all_day" in item_data and data["is_all_day"] is None: - assert item_data["is_all_day"] == i.is_all_day + assert item_data["is_all_day"] == i.is_all_day # ty: ignore[invalid-argument-type] if "time_zone" in item_data and data["time_zone"] is None: - assert item_data["time_zone"] == i.time_zone + assert item_data["time_zone"] == i.time_zone # ty: ignore[invalid-argument-type] if "status" in item_data: - assert item_data["status"] == i.status + assert item_data["status"] == i.status # ty: ignore[invalid-argument-type] if "reminders" in data: assert isinstance(t.reminders, list) - assert len(data["reminders"]) == len(t.reminders) - for reminder_data in data["reminders"]: + assert len(data["reminders"]) == len(t.reminders) # ty: ignore[invalid-argument-type] + for reminder_data in data["reminders"]: # ty: ignore[not-iterable] r = None - assert any(reminder_data["id"] == r.id for r in t.reminders) + assert any(reminder_data["id"] == r.id for r in t.reminders) # ty: ignore[invalid-argument-type] for r in t.reminders: - if r.id == reminder_data["id"]: + if r.id == reminder_data["id"]: # ty: ignore[invalid-argument-type] break assert isinstance(r, TaskReminderV2) - assert reminder_data["id"] == r.id - assert reminder_data["trigger"] == r.trigger + assert reminder_data["id"] == r.id # ty: ignore[invalid-argument-type] + assert reminder_data["trigger"] == r.trigger # ty: ignore[invalid-argument-type] if t.reminder is not None: assert any(t.reminder == r.trigger for r in t.reminders) @@ -262,21 +262,21 @@ def test_get_project_all_wont_do_v2(generate_object_id, delete_projects, client) "title": "test_get_project_all_wont_do_v2_TASK_A", "project_id": project_data["id"], "status": -1, - "completed_time": datetime.now(tz=UTC).isoformat(), + "completed_time": datetime.now(tz=timezone.utc).isoformat(), }, { "id": generate_object_id(), "title": "test_get_project_all_wont_do_v2_TASK_B", "project_id": project_data["id"], "status": -1, - "completed_time": datetime.now(tz=UTC).isoformat(), + "completed_time": datetime.now(tz=timezone.utc).isoformat(), }, { "id": generate_object_id(), "title": "test_get_project_all_wont_do_v2_TASK_C", "project_id": project_data["id"], "status": -1, - "completed_time": datetime.now(tz=UTC).isoformat(), + "completed_time": datetime.now(tz=timezone.utc).isoformat(), }, ] diff --git a/tests/integration/v2/test_task.py b/tests/integration/v2/test_task.py index 9b2a74b2..75381d28 100644 --- a/tests/integration/v2/test_task.py +++ b/tests/integration/v2/test_task.py @@ -163,14 +163,14 @@ def test_create_task_v2(generate_object_id, delete_projects, client, get_batch): expected_task_0_items = [ { - "id": item["id"], - "title": item["title"], - "sortOrder": item["sort_order"], - "isAllDay": item["is_all_day"], - "timeZone": item["time_zone"], - "status": item["status"], + "id": item["id"], # ty: ignore[invalid-argument-type] + "title": item["title"], # ty: ignore[invalid-argument-type] + "sortOrder": item["sort_order"], # ty: ignore[invalid-argument-type] + "isAllDay": item["is_all_day"], # ty: ignore[invalid-argument-type] + "timeZone": item["time_zone"], # ty: ignore[invalid-argument-type] + "status": item["status"], # ty: ignore[invalid-argument-type] } - for item in data[0]["items"] + for item in data[0]["items"] # ty: ignore[not-iterable] ] for task in batch_tasks: if task["title"] == data[0]["title"]: @@ -180,12 +180,12 @@ def test_create_task_v2(generate_object_id, delete_projects, client, get_batch): ) expected_task_1_reminders = [ - {"id": reminder["id"], "trigger": reminder["trigger"]} - for reminder in data[1]["reminders"] + {"id": reminder["id"], "trigger": reminder["trigger"]} # ty: ignore[invalid-argument-type] + for reminder in data[1]["reminders"] # ty: ignore[not-iterable] ] expected_task_2_reminders = [ - {"id": reminder["id"], "trigger": reminder["trigger"]} - for reminder in data[2]["reminders"] + {"id": reminder["id"], "trigger": reminder["trigger"]} # ty: ignore[invalid-argument-type] + for reminder in data[2]["reminders"] # ty: ignore[not-iterable] ] for task in batch_tasks: if task["title"] == data[1]["title"]: diff --git a/tests/unit/test_pydantic.py b/tests/unit/test_pydantic.py index ad81de56..3bc8728a 100644 --- a/tests/unit/test_pydantic.py +++ b/tests/unit/test_pydantic.py @@ -49,9 +49,9 @@ def test_update_model_config_nested(): data = PostBatchTaskV2.model_validate(dict_) assert isinstance(data, PostBatchTaskV2) - assert data.extra_field == "value" # pyright: ignore[reportAttributeAccessIssue] - assert data.add[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] - assert data.add[0].items[0].extra_nested_nested_field == "value" # pyright: ignore[reportOptionalSubscript,reportAttributeAccessIssue] - assert data.update[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] - assert data.update[0].reminders[0].extra_extra_nested_field == "value" # pyright: ignore[reportOptionalSubscript,reportAttributeAccessIssue] - assert data.delete[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] + assert data.extra_field == "value" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] + assert data.add[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] + assert data.add[0].items[0].extra_nested_nested_field == "value" # pyright: ignore[reportOptionalSubscript,reportAttributeAccessIssue] # ty: ignore[possibly-missing-attribute, not-subscriptable] + assert data.update[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] + assert data.update[0].reminders[0].extra_extra_nested_field == "value" # pyright: ignore[reportOptionalSubscript,reportAttributeAccessIssue] # ty: ignore[possibly-missing-attribute, not-subscriptable] + assert data.delete[0].extra_nested_field == "value" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index baed9fd7..88ea7113 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -80,7 +80,7 @@ def test_v1_token_initialize_invalid( test_expiration = None with pytest.raises(ValidationError): - TokenV1(value=test_value, expiration=test_expiration) # pyright: ignore[reportArgumentType] + TokenV1(value=test_value, expiration=test_expiration) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_v1_token_expiration(): diff --git a/uv.lock b/uv.lock index d966b1c0..befbb59d 100644 --- a/uv.lock +++ b/uv.lock @@ -1363,6 +1363,7 @@ dev = [ ] mypy = [ { name = "mypy" }, + { name = "ty" }, { name = "types-icalendar" }, { name = "types-python-dateutil" }, ] @@ -1402,6 +1403,7 @@ dev = [ ] mypy = [ { name = "mypy", specifier = ">=1.15.0" }, + { name = "ty", specifier = ">=0.0.11" }, { name = "types-icalendar", specifier = ">=6.1.3.20250403" }, { name = "types-python-dateutil", specifier = ">=2.9.0.20241206" }, ] @@ -1613,6 +1615,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] +[[package]] +name = "ty" +version = "0.0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/45/5ae578480168d4b3c08cf8e5eac3caf8eb7acdb1a06a9bed7519564bd9b4/ty-0.0.11.tar.gz", hash = "sha256:ebcbc7d646847cb6610de1da4ffc849d8b800e29fd1e9ebb81ba8f3fbac88c25", size = 4920340, upload-time = "2026-01-09T21:06:01.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/34/b1d05cdcd01589a8d2e63011e0a1e24dcefdc2a09d024fee3e27755963f6/ty-0.0.11-py3-none-linux_armv6l.whl", hash = "sha256:68f0b8d07b0a2ea7ec63a08ba2624f853e4f9fa1a06fce47fb453fa279dead5a", size = 9521748, upload-time = "2026-01-09T21:06:13.221Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/f52d93f4b3784b91bfbcabd01b84dc82128f3a9de178536bcf82968f3367/ty-0.0.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbf82d7ef0618e9ae3cc3c37c33abcfa302c9b3e3b8ff11d71076f98481cb1a8", size = 9454903, upload-time = "2026-01-09T21:06:42.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/3a563dba8b1255e474c35e1c3810b7589e81ae8c41df401b6a37c8e2cde9/ty-0.0.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:121987c906e02264c3b511b95cb9f8a3cdd66f3283b8bbab678ca3525652e304", size = 8823417, upload-time = "2026-01-09T21:06:26.315Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b1/99b87222c05d3a28fb7bbfb85df4efdde8cb6764a24c1b138f3a615283dd/ty-0.0.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:999390b6cc045fe5e1b3da1c2c9ae8e8c0def23b69455e7c9191ba9ffd747023", size = 9290785, upload-time = "2026-01-09T21:05:59.028Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9f/598809a8fff2194f907ba6de07ac3d7b7788342592d8f8b98b1b50c2fb49/ty-0.0.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed504d78eb613c49be3c848f236b345b6c13dc6bcfc4b202790a60a97e1d8f35", size = 9359392, upload-time = "2026-01-09T21:06:37.459Z" }, + { url = "https://files.pythonhosted.org/packages/71/3e/aeea2a97b38f3dcd9f8224bf83609848efa4bc2f484085508165567daa7b/ty-0.0.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fedc8b43cc8a9991e0034dd205f957a8380dd29bfce36f2a35b5d321636dfd9", size = 9852973, upload-time = "2026-01-09T21:06:21.245Z" }, + { url = "https://files.pythonhosted.org/packages/72/40/86173116995e38f954811a86339ac4c00a2d8058cc245d3e4903bc4a132c/ty-0.0.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0808bdfb7efe09881bf70249b85b0498fb8b75fbb036ce251c496c20adb10075", size = 10796113, upload-time = "2026-01-09T21:06:16.034Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/97c92c401dacae9baa3696163ebe8371635ebf34ba9fda781110d0124857/ty-0.0.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07185b3e38b18c562056dfbc35fb51d866f872977ea1ebcd64ca24a001b5b4f1", size = 10432137, upload-time = "2026-01-09T21:06:07.498Z" }, + { url = "https://files.pythonhosted.org/packages/18/10/9ab43f3cfc5f7792f6bc97620f54d0a0a81ef700be84ea7f6be330936a99/ty-0.0.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5c72f1ada8eb5be984502a600f71d1a3099e12fb6f3c0607aaba2f86f0e9d80", size = 10240520, upload-time = "2026-01-09T21:06:34.823Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/8dd4fe6df1fd66f3e83b4798eddb1d8482d9d9b105f25099b76703402ebb/ty-0.0.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25f88e8789072830348cb59b761d5ced70642ed5600673b4bf6a849af71eca8b", size = 9973340, upload-time = "2026-01-09T21:06:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/fb2301450cf8f2d7164944d6e1e659cac9ec7021556cc173d54947cf8ef4/ty-0.0.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f370e1047a62dcedcd06e2b27e1f0b16c7f8ea2361d9070fcbf0d0d69baaa192", size = 9262101, upload-time = "2026-01-09T21:06:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/f7/8c/d6374af023541072dee1c8bcfe8242669363a670b7619e6fffcc7415a995/ty-0.0.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:52be34047ed6177bfcef9247459a767ec03d775714855e262bca1fb015895e8a", size = 9382756, upload-time = "2026-01-09T21:06:24.097Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/edd1e63ffa8d49d720c475c2c1c779084e5efe50493afdc261938705d10a/ty-0.0.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9e5762ccb3778779378020b8d78f936b3f52ea83f18785319cceba3ae85d8e6", size = 9553944, upload-time = "2026-01-09T21:06:18.426Z" }, + { url = "https://files.pythonhosted.org/packages/35/cd/4afdb0d182d23d07ff287740c4954cc6dde5c3aed150ec3f2a1d72b00f71/ty-0.0.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9334646ee3095e778e3dbc45fdb2bddfc16acc7804283830ad84991ece16dd7", size = 10060365, upload-time = "2026-01-09T21:06:45.083Z" }, + { url = "https://files.pythonhosted.org/packages/d1/94/a009ad9d8b359933cfea8721c689c0331189be28650d74dcc6add4d5bb09/ty-0.0.11-py3-none-win32.whl", hash = "sha256:44cfb7bb2d6784bd7ffe7b5d9ea90851d9c4723729c50b5f0732d4b9a2013cfc", size = 9040448, upload-time = "2026-01-09T21:06:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/5a5dfd0aec0ea99ead1e824ee6e347fb623c464da7886aa1e3660fb0f36c/ty-0.0.11-py3-none-win_amd64.whl", hash = "sha256:1bb205db92715d4a13343bfd5b0c59ce8c0ca0daa34fb220ec9120fc66ccbda7", size = 9780112, upload-time = "2026-01-09T21:06:04.69Z" }, + { url = "https://files.pythonhosted.org/packages/ad/07/47d4fccd7bcf5eea1c634d518d6cb233f535a85d0b63fcd66815759e2fa0/ty-0.0.11-py3-none-win_arm64.whl", hash = "sha256:4688bd87b2dc5c85da277bda78daba14af2e66f3dda4d98f3604e3de75519eba", size = 9194038, upload-time = "2026-01-09T21:06:10.152Z" }, +] + [[package]] name = "types-icalendar" version = "6.3.2.20251222"