From b5f09b384f28be443f0f8746ff9c8e11b226e24a Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Sun, 14 Dec 2025 19:34:32 -0500 Subject: [PATCH 01/33] workaround a dateparser bug --- workedon/parser.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/workedon/parser.py b/workedon/parser.py index bcde6a5..bc916e9 100644 --- a/workedon/parser.py +++ b/workedon/parser.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta import re from typing import Final @@ -43,7 +43,13 @@ def parse_datetime(self, date_time: str) -> datetime: parsed_dt = self._as_datetime(dt) if not parsed_dt: raise InvalidDateTimeError() - if parsed_dt > now(): + current = now() + # workaround a dateparser bug: even though we set PREFER_DATES_FROM to "past", + # it sometimes still returns a future date when only time is given. + # If parsed time is on the same day and still in the future, shift to previous day + if parsed_dt.date() == current.date() and parsed_dt > current: + parsed_dt = parsed_dt - timedelta(days=1) + if parsed_dt > current: raise DateTimeInFutureError() return parsed_dt From 38dc03ec3641940257ae3e5ab8edd0646eff22b8 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Sun, 14 Dec 2025 20:15:24 -0500 Subject: [PATCH 02/33] select UUID for efficient delete-subquery --- workedon/workedon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/workedon/workedon.py b/workedon/workedon.py index d2a31be..fcd9395 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -136,8 +136,10 @@ def fetch_work( Fetch saved work filtered based on user input """ # filter fields - fields = [] - if not delete: + if delete: + # Ensure we select UUID for efficient delete-subquery + fields = [Work.uuid] + else: fields = [Work.work] if text_only else [Work.uuid, Work.timestamp, Work.work, Work.duration] # initial set From 5e8ea8b40e33e8ad75786b10cdd4895a8071a720 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Sun, 14 Dec 2025 21:02:16 -0500 Subject: [PATCH 03/33] use prefetch to get tag data --- workedon/models.py | 8 +++++++- workedon/workedon.py | 11 +++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/workedon/models.py b/workedon/models.py index bb680ec..9c26ca7 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -82,7 +82,13 @@ def __str__(self) -> str: timestamp_str = user_time.strftime( settings.DATETIME_FORMAT or f"{settings.DATE_FORMAT} {settings.TIME_FORMAT}" ) - tags = [t.tag.name for t in self.tags.order_by(WorkTag.tag.name)] + # Handle both prefetched tags (list) and lazy-loaded tags (query) + if isinstance(self.tags, list): + # Tags are prefetched as a list, sort in Python + tags = sorted([t.tag.name for t in self.tags]) + else: + # Tags are lazy-loaded, use SQL ordering + tags = [t.tag.name for t in self.tags.order_by(WorkTag.tag.name)] tags_str = f"Tags: {', '.join(tags)}\n" if tags else "" if self.duration is not None: diff --git a/workedon/workedon.py b/workedon/workedon.py index fcd9395..5eb6d63 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -9,7 +9,7 @@ from typing import Any import click -from peewee import ModelSelect, chunked +from peewee import ModelSelect, chunked, prefetch from .constants import WORK_CHUNK_SIZE from .exceptions import ( @@ -199,12 +199,15 @@ def fetch_work( click.echo("Nothing to show, slacker.") return + # Prefetch tags to avoid N+1 queries (only when displaying full format) + work_result = work_set if text_only else prefetch(work_set, WorkTag, Tag) + if work_count == 1 or no_page: - for work in work_set: + for work in work_result: click.echo(work, nl=False) else: - gen = work_set.iterator() - click.echo_via_pager(_generate_work(gen)) + gen = _generate_work(work_result.iterator()) + click.echo_via_pager(gen) except Exception as e: raise CannotFetchWorkError(extra_detail=str(e)) from e From ebc3a84cce7c7f36d6fe088de30efb7fa33177d1 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Sun, 21 Dec 2025 15:49:47 -0500 Subject: [PATCH 04/33] add index for duration --- workedon/constants.py | 2 +- workedon/models.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/workedon/constants.py b/workedon/constants.py index fc8ddf9..602a601 100644 --- a/workedon/constants.py +++ b/workedon/constants.py @@ -8,5 +8,5 @@ # See https://github.com/viseshrp/workedon#settings for more information. # """ -CURRENT_DB_VERSION: Final[int] = 2 +CURRENT_DB_VERSION: Final[int] = 3 WORK_CHUNK_SIZE: Final[int] = 100 diff --git a/workedon/models.py b/workedon/models.py index 9c26ca7..7bd7937 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -70,7 +70,7 @@ class Work(Model): index=True, default=get_default_time, ) - duration: FloatField = FloatField(null=True, default=None) + duration: FloatField = FloatField(null=True, default=None, index=True) def __str__(self) -> str: """ @@ -199,11 +199,27 @@ def _migrate_v1_to_v2(database: SqliteDatabase) -> None: _set_db_user_version(database, 2) +def _migrate_v2_to_v3(database: SqliteDatabase) -> None: + """ + Migrate from v2 → v3: add indexes for better query performance. + Adds indexes on Work.duration, WorkTag.work, and WorkTag.tag. + Then bump to v3. + """ + # Add indexes for query optimization using migrator + migrator = SqliteMigrator(database) + migrate( + migrator.add_index("work", ("duration",), False), + ) + # bump the version to 3 + _set_db_user_version(database, 3) + + def _apply_pending_migrations(database: SqliteDatabase) -> None: """ Check PRAGMA user_version on the disk. - - If it's 0, do the initial create (v0 → v2 in one shot). + - If it's 0, do the initial create (v0 → v3 in one shot). - Else if it's 1, run v1 -> v2. + - Else if it's 2, run v2 -> v3. """ try: existing_version = get_db_user_version(database) @@ -215,6 +231,10 @@ def _apply_pending_migrations(database: SqliteDatabase) -> None: if existing_version < 2: _migrate_v1_to_v2(database) existing_version = get_db_user_version(database) + # v2 + if existing_version < 3: + _migrate_v2_to_v3(database) + existing_version = get_db_user_version(database) # Add more future versions here... # sanity check if existing_version != CURRENT_DB_VERSION: From c2259205d9aad1aad85bd5206ecdb280b844af25 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 13:40:02 -0500 Subject: [PATCH 05/33] Revert "use prefetch to get tag data" This reverts commit 5e8ea8b40e33e8ad75786b10cdd4895a8071a720. --- workedon/models.py | 8 +------- workedon/workedon.py | 11 ++++------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/workedon/models.py b/workedon/models.py index 7bd7937..564e168 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -82,13 +82,7 @@ def __str__(self) -> str: timestamp_str = user_time.strftime( settings.DATETIME_FORMAT or f"{settings.DATE_FORMAT} {settings.TIME_FORMAT}" ) - # Handle both prefetched tags (list) and lazy-loaded tags (query) - if isinstance(self.tags, list): - # Tags are prefetched as a list, sort in Python - tags = sorted([t.tag.name for t in self.tags]) - else: - # Tags are lazy-loaded, use SQL ordering - tags = [t.tag.name for t in self.tags.order_by(WorkTag.tag.name)] + tags = [t.tag.name for t in self.tags.order_by(WorkTag.tag.name)] tags_str = f"Tags: {', '.join(tags)}\n" if tags else "" if self.duration is not None: diff --git a/workedon/workedon.py b/workedon/workedon.py index 5eb6d63..fcd9395 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -9,7 +9,7 @@ from typing import Any import click -from peewee import ModelSelect, chunked, prefetch +from peewee import ModelSelect, chunked from .constants import WORK_CHUNK_SIZE from .exceptions import ( @@ -199,15 +199,12 @@ def fetch_work( click.echo("Nothing to show, slacker.") return - # Prefetch tags to avoid N+1 queries (only when displaying full format) - work_result = work_set if text_only else prefetch(work_set, WorkTag, Tag) - if work_count == 1 or no_page: - for work in work_result: + for work in work_set: click.echo(work, nl=False) else: - gen = _generate_work(work_result.iterator()) - click.echo_via_pager(gen) + gen = work_set.iterator() + click.echo_via_pager(_generate_work(gen)) except Exception as e: raise CannotFetchWorkError(extra_detail=str(e)) from e From 3da33fe99291b52599b6261b58e3bd96b03d7244 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 15:16:58 -0500 Subject: [PATCH 06/33] Update uv.lock --- uv.lock | 801 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 408 insertions(+), 393 deletions(-) diff --git a/uv.lock b/uv.lock index 890954f..5839edf 100644 --- a/uv.lock +++ b/uv.lock @@ -40,97 +40,107 @@ wheels = [ [[package]] name = "backports-zstd" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/403d81c1b9264f6ee1c25b2f2ce892ed9930d1a0a1939ef26a01a95e644d/backports_zstd-1.1.0.tar.gz", hash = "sha256:8ce84125e3094ddefbaa25306e8949965ce092b400a1fd63b5e6a5e932d2994f", size = 996020, upload-time = "2025-11-23T12:51:19.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/bb/9f71f34b1c7ad957f36ca320bcdfef1836816af9a8f5d256debe5f12bad6/backports_zstd-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f02ab91901b637fae5d27cc26f02874db3e5f98af481afafb86d6487a4287897", size = 435750, upload-time = "2025-11-23T12:49:03.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9a/f064362e44419625b79e049dc532c7b7606bd072f230d2729460cf4f5da8/backports_zstd-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5805a215a3b048a10ca18dd5c525c737e004e59c92c21c44baa0c13083579dc1", size = 362145, upload-time = "2025-11-23T12:49:04.823Z" }, - { url = "https://files.pythonhosted.org/packages/69/c7/e163febeb3504a00f3a7625d40a2d1a120298762393caebe618e96c48594/backports_zstd-1.1.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:777ff9a60664abfa7e5cbcb676ecbedf49bd567d6184e538b9dee09d4bbee292", size = 506045, upload-time = "2025-11-23T12:49:05.951Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a9/d28aa8f16a9fafd8e15220f3ac32fcd000589088f45c80df7f45839503cc/backports_zstd-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27a40d9518019a6b6ad2df5f6c2dd6ce627af9396c929556188a67607ce12d57", size = 475659, upload-time = "2025-11-23T12:49:07.463Z" }, - { url = "https://files.pythonhosted.org/packages/a3/94/9c9b2aa30d34e83f244057c6bf7b2a493c7d60df1caa999160bf83b5a525/backports_zstd-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24e73d5d4b3c1b9c693f07081703db9e9398ce95576f1ed74892d6bd01adcb4", size = 581286, upload-time = "2025-11-23T12:49:09.095Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/09f21da99977d3b220c8a4642fa1218b2b5d854fb2f00eb22dc38b02be3e/backports_zstd-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:097ea6090dfb60f4af3c09d816c6d64175bd94ead930e5089acd00ea2e5b8de8", size = 640931, upload-time = "2025-11-23T12:49:10.341Z" }, - { url = "https://files.pythonhosted.org/packages/42/cb/d765b8f70e86e368d05632572e7eec3dd27d6bfb61c9c7867b3375b3f19e/backports_zstd-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0495547f5b727d798ff317b6cc6158655926b7149739ab9a193e184d65e4bc8f", size = 491149, upload-time = "2025-11-23T12:49:11.524Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1f/6ad1c8af282dc6ad464ff58ecfb29abaeeae6f745c45e5500723f8a0c1e2/backports_zstd-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:544494f7f1ba88f6500a5e14219b19718cc2aa4bcc563ce78d67aef3b8e54ed5", size = 481607, upload-time = "2025-11-23T12:49:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0a/6f82529599e92af47bd483bf3c318bd14c01e5a00b9a736d9bc7ab1db3b0/backports_zstd-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:559fe9384dbd43cdacc2b4aa7e19199f064da7aec947c6461d4a7aadfb614568", size = 509552, upload-time = "2025-11-23T12:49:14.166Z" }, - { url = "https://files.pythonhosted.org/packages/39/5a/87e4be4c18c920122949d45a9de7e253084af22ae50b3c0773e4d58aa30a/backports_zstd-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6e7ffa9747051fd994f8070aefc8317af3ce485d9dbd175593c6df9d795d24ff", size = 585650, upload-time = "2025-11-23T12:49:15.379Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4c/4afff28eb575631882d564b6c8b93f4d43e53f99c62ceb03f51b15400f9a/backports_zstd-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:27858ea429914fc67446112fd7797e821d18e7ce3d6cfa7b2e124547c6a4ca83", size = 631508, upload-time = "2025-11-23T12:49:16.89Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8d/f2564d8fe96398bc15ccd4db605d448a8dc4d6dc4e1e3bc613c6f2306b87/backports_zstd-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:233f35525940b5153223909a4c9f554e27ae7f5bff5c946b9fe28b633cdd463a", size = 495209, upload-time = "2025-11-23T12:49:18.083Z" }, - { url = "https://files.pythonhosted.org/packages/36/47/9eb4a27599af4501a79d9d80d6616a18b8cdf52683b1c711d403d247f6a2/backports_zstd-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be5723019ba1500cf727a6ba37a15e697a63f66314e3e7def52fd86c65249236", size = 288660, upload-time = "2025-11-23T12:49:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e8/e3f70c4835338167d7039152b8cb1c4adae8c14fed7a184f2d860b0bb952/backports_zstd-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c28905252c4273e7097baad10ca786ff8e5ab67807f428504c4fe3cbdecb135e", size = 313570, upload-time = "2025-11-23T12:49:20.706Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bf/12f84454d23cd32a649adbc49983fed13ae2f60a75a6461e9db6e435d856/backports_zstd-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:bd2e69a69524161e20cad8245dcbacd9b825f83221c55b05bca0e506b7abddfd", size = 288773, upload-time = "2025-11-23T12:49:22.226Z" }, - { url = "https://files.pythonhosted.org/packages/73/81/47935ce4c225681a2a962c69cdc061f721bc02c6b2d9806d0ba4d29a1454/backports_zstd-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b1f9ea3d2d3638af80fcd896dceba7ad5d902ce3e5b411a9b16e4cddd79c4ac", size = 435751, upload-time = "2025-11-23T12:49:23.738Z" }, - { url = "https://files.pythonhosted.org/packages/c1/36/85f89ea19338e7babfb91147ad3f4022c736c0bcbc1ebf83c90550f2722c/backports_zstd-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfa36e208619fd152c20bd6c9d0febcb18ac90bdd811421c6a01e450f4b1dd6d", size = 362143, upload-time = "2025-11-23T12:49:25.245Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/f722122cbd44331245c674c9d3dac91df135d491348de7da5010980d346d/backports_zstd-1.1.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9f43275263cfbd2e9096a8b333d15c2e96deb0a2df3751b15408155a03c57fab", size = 506044, upload-time = "2025-11-23T12:49:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/41d5088c35bbbd03a07e0f315566e06f915b0371700f568e8c3b26c68b40/backports_zstd-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1c6ae75c09f211b4f5439b0984bfb8b8d15975585e50b1dc658e9f9b94bd305", size = 475657, upload-time = "2025-11-23T12:49:28.056Z" }, - { url = "https://files.pythonhosted.org/packages/d1/14/324476e8eef484779c390d0b9c83b7906c883ae82b7de4651a86463fbb71/backports_zstd-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d38c6df19d5b828cb3d1c1ab62a2c61dd433ad00523d577e303f4552fa002b0", size = 581283, upload-time = "2025-11-23T12:49:29.536Z" }, - { url = "https://files.pythonhosted.org/packages/bd/01/4f61e96f52dd391834dd457ee970b57d2818609677a786f94840da359f02/backports_zstd-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64d5c359a04c26a36eaad82386a873ff1af9e12f896af4748cff1f35e2550af3", size = 640975, upload-time = "2025-11-23T12:49:30.916Z" }, - { url = "https://files.pythonhosted.org/packages/57/79/69b97eea66a26f52975ae78d7413ca79efee51d2349a33ddfef9b196302c/backports_zstd-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:773cec5a3ff8c81e03f3db0c7df1d2f7c3cf820262138ec752c46fd25b532e85", size = 491188, upload-time = "2025-11-23T12:49:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/5d402a41c29121f387be38e3a507ba6c3d9c5304356f659beba8bcdadf80/backports_zstd-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:be97026835fb5cf0cb96e8c1c6ee72e53816f0ff34c368546ea6e53962476216", size = 481610, upload-time = "2025-11-23T12:49:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/85/9d/25dceef8e97fb815259b3c946236ff6e696bb5f461faa8303dca5d98e4e8/backports_zstd-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6fb2b381a404040c9dbfd258f3bcd740595e2aca088d822fb241942440fce509", size = 509555, upload-time = "2025-11-23T12:49:34.675Z" }, - { url = "https://files.pythonhosted.org/packages/bd/74/fb0883e9dcc6333f33f3f90e1101498800b82b6437cfa110244c59d6a03e/backports_zstd-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:adac539e6fbcf72d70cd9f354ab5689e080b8621b4d268f2ba0f486c25dfd3f3", size = 585639, upload-time = "2025-11-23T12:49:36.033Z" }, - { url = "https://files.pythonhosted.org/packages/88/ad/6db9224a34d0ef6ce553c9605649a522b485efc0aa098b1c78b4bcdaad5a/backports_zstd-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4298631dc91b4dafc20b6a7eb7edb028c341bc2e3b77a128c7ced5148aaa215c", size = 631550, upload-time = "2025-11-23T12:49:37.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/a5/383d1d9b2281854b2809b83ec6dfe9092f3a8b9700055340176b29230b1c/backports_zstd-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62451464559525ff093f7d684bf88381722d7b3759693ea842e94b41c01f306b", size = 495212, upload-time = "2025-11-23T12:49:38.814Z" }, - { url = "https://files.pythonhosted.org/packages/cd/75/305ac0bf93de4a771d33926e27da401a807294c3858b133d3611d0b6be49/backports_zstd-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b20309c15d4afa2919517669366da60dd30955dbc0784924ae79acb49f158ef6", size = 288753, upload-time = "2025-11-23T12:49:39.972Z" }, - { url = "https://files.pythonhosted.org/packages/50/f3/8887a706bef88307a55719b4d0cf73600c6703e3cfb5926f97441c8ffc3b/backports_zstd-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:9e3df9c43e9f4ed970cd8f48df41736c5697fed06523e463661788762c95d454", size = 313698, upload-time = "2025-11-23T12:49:41.202Z" }, - { url = "https://files.pythonhosted.org/packages/3c/ea/78f627df9b3765ac7036da11951db7c91eb77e6014f8819dbf575dc57f8e/backports_zstd-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5c43022248bd8711f606a481547dbcf4541b617446a07a359aad28ebf77973b", size = 288897, upload-time = "2025-11-23T12:49:42.306Z" }, - { url = "https://files.pythonhosted.org/packages/48/98/1426b121772c4f05735ad7c9542dd333c18720179a549dd2e22e6be386df/backports_zstd-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:63e1ba127129c7c4786147f873eef63f90694a13c5459ca0e970cad1bd52cf6c", size = 436062, upload-time = "2025-11-23T12:49:43.409Z" }, - { url = "https://files.pythonhosted.org/packages/4b/01/c851ce0cbe2611ce33232ecaa21f237b9b3c52964f01c9e96cabc7f60b0f/backports_zstd-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37b30c0fb5d118344706ed9e26cb2f43584c0012565254a819485bab180f1652", size = 362213, upload-time = "2025-11-23T12:49:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/af/c0/99596faafe038ba03695569adf0ec1f4e3f955c3156618b0f11deb3b9905/backports_zstd-1.1.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:b8b77fae55a245e5b61fc599b641343da2580ebc9fcb20a77822dcca9126bddc", size = 506466, upload-time = "2025-11-23T12:49:46.085Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/b6410324c8f495bd6fd1c8ab8b3fbb70120ea2b06ee004db4549e0eb58b0/backports_zstd-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34c9c425d113071c90a241f04fb12be46c83e7028a090aca4e72fe9f5fe11fb6", size = 476287, upload-time = "2025-11-23T12:49:47.327Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7a/280f37869fb37dc16d2ca093734a6cac564a551d7fad8b55d0807d19f74c/backports_zstd-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a7c9c2bd44b5bbfba2c78411feb2e0074406654f33e6b0317e307710c7fe57", size = 581744, upload-time = "2025-11-23T12:49:48.907Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/828ce97c1c18d3735ca42b65819241702df101cfd13a4b88bd77ca1f03bd/backports_zstd-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3baf8b13aa90ed5a03503063be670d8cca22d5d81aab4cc499741f013dba4351", size = 640474, upload-time = "2025-11-23T12:49:50.184Z" }, - { url = "https://files.pythonhosted.org/packages/06/ad/5ba858ffc307ed2b51693d87642f9e9dd6013579840981ac364c6e24c38c/backports_zstd-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe522558c4d56d9eb2294592fec5ac94b95c82ff22cf1f91d1bf589fe567f391", size = 494260, upload-time = "2025-11-23T12:49:51.765Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4c/3b2f67ae51d19716af5d9bd2c074b9a5ba03a7f6ef4c509adaee3e1f6958/backports_zstd-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f459c3b43d382aa2ab44199b84ec86a33f79094701bd3bbf536d4b6e687e744", size = 482320, upload-time = "2025-11-23T12:49:52.943Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e0/05f1d8a8552254ab4ed447ad2950c079f15bd2c74125a8a15fa9f0eb28ef/backports_zstd-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b53841a136c326686d49f1b2b96dc5acdffb2e8e422a5298f36b95a7d927b69a", size = 509894, upload-time = "2025-11-23T12:49:54.149Z" }, - { url = "https://files.pythonhosted.org/packages/62/0d/15dc13edf653ba14f61f9ce0a4119f79647750a9525659b410be124b5d71/backports_zstd-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d64a172db16fa5e3aed875776537103cae0f2dd0e614c9658fce62ebd5211847", size = 586126, upload-time = "2025-11-23T12:49:55.363Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c9/9ece6a0c684223e466188d7e8db74462075391f43be244f46ef46ce1f07a/backports_zstd-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de5d7cdea31085b3d21c7f670f385ec666b8595c72c107efa50e29f3cc4e72a1", size = 630934, upload-time = "2025-11-23T12:49:57.115Z" }, - { url = "https://files.pythonhosted.org/packages/d9/da/9c704e8f11cc4cb17adae9693d97903fe9dc5ea7e8182d044c00cf70fb1d/backports_zstd-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8dc11ddef5529aff9fa18565a0f2a98f9f5cff83751e4357c219ae20c160bc1f", size = 498755, upload-time = "2025-11-23T12:49:58.313Z" }, - { url = "https://files.pythonhosted.org/packages/45/ac/843bc205463fce6aff56d2d6f8733635b0c8786c22d148978ecfed97645f/backports_zstd-1.1.0-cp312-cp312-win32.whl", hash = "sha256:8e0376da95159b39f538b6d7edafc211f4b9e7f5581ad4b2476b0f3c890bb25f", size = 288892, upload-time = "2025-11-23T12:49:59.569Z" }, - { url = "https://files.pythonhosted.org/packages/43/aa/dbee1f51bf3af6017e1b48b93edf9c60f74a57c7d34940a728789365355e/backports_zstd-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:8067a95f560c503d683b9e18de44749eefe0a4ad6da318ff9eabb4afe47e79c5", size = 313910, upload-time = "2025-11-23T12:50:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/66/8f/cc99ca99a2e37a9a9f3c6053cd0a7bd255c465be24d1d931fe062dca55bd/backports_zstd-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:f8f2a975dd25b4b3ac901ce47d04881d43dcde18037d2d1513b8a5f4fededdd8", size = 289014, upload-time = "2025-11-23T12:50:02.226Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/2a50f48bd16a4677b1f17f0ab0d173f9225114100326e67fdd65b74f9586/backports_zstd-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a18668f1373fe9e5e0349e3fe5cc8148cbef29eccbb9998993eeb0e90d35638", size = 435551, upload-time = "2025-11-23T12:50:03.881Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ee/10ddf48f87fe8aeedd4dca1a18061159eab006ede693ede00ba80946ba3a/backports_zstd-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3adf32f6e58323505c581a4d1e5b6404e98806c4cf01c54049c372642cf7ec6d", size = 361827, upload-time = "2025-11-23T12:50:04.999Z" }, - { url = "https://files.pythonhosted.org/packages/4c/56/5d98bd5a7b0a5d203f705f3200b467af6a6d34fa6d9aa2b9c9538369dae8/backports_zstd-1.1.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:b71f45d12274021e1d7b689ac98faff94874cf6c7bac2f0ab14a3714d4047a6c", size = 505739, upload-time = "2025-11-23T12:50:06.496Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/5a3967e957fc5662eaaeca4ace5eb0646d1e174e70d149fe023092165047/backports_zstd-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd98ea2600335bd941bba347e168462c37a5ed1bdc21cbee2dce82871c3719b3", size = 475946, upload-time = "2025-11-23T12:50:07.737Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f6/71c99d08987df24e09b93fde4a2d3aeee8075563374be88572a1f265f9a5/backports_zstd-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83143a2a381d95276a8df5f0b6231f936595d3aeb15a55f91831a111d78297f4", size = 581425, upload-time = "2025-11-23T12:50:09.091Z" }, - { url = "https://files.pythonhosted.org/packages/79/a8/8788b74b53961b3d3dfb08eb4632d07c846afbc2f677752d9f2ae0d6b843/backports_zstd-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7caf0d5c2954eea1c160922207afd87cd83cef7f5081b5a7b12d4a646a78c1a2", size = 642271, upload-time = "2025-11-23T12:50:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/f1/08/211d882fb5fcee5014bac84c64acf4c6f5c6df3a10e3bb5ed20409b5e54c/backports_zstd-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:222fb59203bb7c8104d049c587cd60bee6e8c1cd6d34c4a87158b8393dc8113a", size = 490893, upload-time = "2025-11-23T12:50:11.623Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8b/8a2625657cc9965c62a56fb2d97ad4cdd31e8cbdb6aad493375c337ac3fe/backports_zstd-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d74065d6b151443b25f3005ae96dac1d896ce616c56595c18c7dfbea77b73a5a", size = 481704, upload-time = "2025-11-23T12:50:12.896Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/f3b225722e3f1544a1c8cdedc92a35650f6b4ce0ee1444db949240c40efa/backports_zstd-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf5aa4431001eca56bac39e22e9c814075008d9be7317271e5f8bff21533086d", size = 509295, upload-time = "2025-11-23T12:50:14.409Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/2f1335f7cf06de3140612ac72bb0735c4d8e973f15678f5e62ba752a16b0/backports_zstd-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c21cbeeba96d4b15e42ace21fd71f0bf2c431247ce66e82409c0f45dc7d7b4b", size = 585856, upload-time = "2025-11-23T12:50:15.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6d/0001f8c654672d5ac309f315919b4d0be26c1687b46b2c9438bff4d16895/backports_zstd-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:79ab438599cedf19cce056147a26c64167bf83d05609b4481ffadf9330a322cf", size = 632636, upload-time = "2025-11-23T12:50:16.855Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/5f1835d53ce4b66e445aeca019c516c3989d74269e96c58295b8cd629421/backports_zstd-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8389d1edc351af4c5e6cef9fcd41f0795d7e639becfb0f331cec67aef3eefa3", size = 495191, upload-time = "2025-11-23T12:50:19.015Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f0/82b912cd9bec18cec86466300372ad2913febe4c7066eb6a6721eada9777/backports_zstd-1.1.0-cp313-cp313-win32.whl", hash = "sha256:f862567e43a989be9c0d1a04c9d3068a8544a1a48c908635ecaffcb896f2c1b1", size = 288529, upload-time = "2025-11-23T12:50:20.234Z" }, - { url = "https://files.pythonhosted.org/packages/d7/07/182dc5e7c17206fc07f30c3786317fa82f18b124f037a5b8dbb73457a341/backports_zstd-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e93b0a04f624ebe5b4301ec4997b353db561b82e7e21b42c84c8b30c53218a8", size = 313745, upload-time = "2025-11-23T12:50:21.845Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f7/3418b0e3c15865c24fc390d57e33e141ea8f7347876e4a1feb2942e5abc7/backports_zstd-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:47ed787d85ea29776bc7a54825dd2bd6431a1497bb120d843f4dbacabb4f0d65", size = 288762, upload-time = "2025-11-23T12:50:23.114Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7c/7f62db771263bf5596489c2a5574278ebf4095980f6ee01305b7989ca78e/backports_zstd-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:01e488a5f1b008643a375936b655f383e92bd02b6b7830c4f4489eb9ede2661d", size = 436239, upload-time = "2025-11-23T12:50:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1b/b336683e6e470ddc4f3665588327cef4c96da876469ad3640e3af6d0c502/backports_zstd-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:feef6c896d58f93e8fb9c900eb5b0813580a3a689a7bab53a82d7980599952bb", size = 362524, upload-time = "2025-11-23T12:50:25.892Z" }, - { url = "https://files.pythonhosted.org/packages/a0/23/e0497e863e37f7c659ee5037a743809d82270b84090fa4c7d3564312d4d4/backports_zstd-1.1.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:91c741b17240469f414786051dadc24372a2270c7664908159d19075a6e58455", size = 507685, upload-time = "2025-11-23T12:50:27.497Z" }, - { url = "https://files.pythonhosted.org/packages/be/cc/fa3d8656e0202030304c7d9de8da3e02157812ab04a5d726314073b04e61/backports_zstd-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36d898fc5b592b9822a1b2c90d41c3fc4b684579ef558e6b08b53a747ee542", size = 475585, upload-time = "2025-11-23T12:50:28.834Z" }, - { url = "https://files.pythonhosted.org/packages/6c/21/c6effc30caef1dd8db49a962e554601f1ea5782218f38e16013f719a5711/backports_zstd-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:37cc049f855654f76fd145c32f3818848848c0128940f2e0a2e0f5597e3658a9", size = 581008, upload-time = "2025-11-23T12:50:30.052Z" }, - { url = "https://files.pythonhosted.org/packages/18/d2/bf2f74d68fde7884295138fe0ac8049031d8061346c395a61ebc256dd470/backports_zstd-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30414d5d15ff358a1cbb88b293a108a2eebaed6ccf034176e2a4ffcc80672df", size = 639999, upload-time = "2025-11-23T12:50:31.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/63/ea63603b691c54a6af0259b4c74c218cc9ba14985bd18ecd629d735c81ec/backports_zstd-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:747c341023725ea83e5ab874325e85dc89d9008626bbbc329c40426bedd03ecb", size = 494888, upload-time = "2025-11-23T12:50:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/55/c0/0a33c6036f8350954cb9f99363710c4e6589ba72e38dfe181a117c983fc2/backports_zstd-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:05b5d4f23b22f67b64825537e9c4d2e6a38914e924794803ee2c4f7a9715b46d", size = 482069, upload-time = "2025-11-23T12:50:33.862Z" }, - { url = "https://files.pythonhosted.org/packages/dc/40/4a955634f174f569aa8a03433148818528736ec66d5baa4bce2f535899ad/backports_zstd-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:284d91e68e67ee7b8d2c58f26032bb15e4ed6ec9f2b95121af0b9c260741e497", size = 511445, upload-time = "2025-11-23T12:50:35.09Z" }, - { url = "https://files.pythonhosted.org/packages/19/bf/8d99e2fb714e908b0282a432f9216968db46ed5b6ec42bdc9df5b0ffbea9/backports_zstd-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:9eb74ca6f9afa8d92eb268dff951d901ef3366a240ca358d1503da9a4355eab5", size = 585581, upload-time = "2025-11-23T12:50:36.745Z" }, - { url = "https://files.pythonhosted.org/packages/14/9f/768d64b30e80b32e262393caf2784f5fd43b45e6ab8e2a44c146a550c607/backports_zstd-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b3bef8f715a4f8135709846179fb748d4ec5619282a8858cdeaf90ab9a3c1cd9", size = 630606, upload-time = "2025-11-23T12:50:37.937Z" }, - { url = "https://files.pythonhosted.org/packages/cd/69/41c3f1d2f97cb156e689e36f3ff8ddb9b1140f667394ce0b3d51f0a96012/backports_zstd-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aba8cb745d48e66d5e9f007cbf6871e3a227b2812a81c99916de3f8edc2cfaa1", size = 499515, upload-time = "2025-11-23T12:50:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/92/bb10b678aa8038172047adfd748b73b410c34aa8fefac4768128d79dbc2d/backports_zstd-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:d13e6406a50051f2bdd5047dc68e79e398c4d4bf65de910a1cd02e2d93f6d346", size = 289476, upload-time = "2025-11-23T12:50:40.365Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/f1763f8f1cee20b2a836c1aa27a545cb1ae053bc1e282716f5062b9f6169/backports_zstd-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:029388257045a1bd091407ed4359899bb07ddac786ebd240ce6985b82da5729b", size = 314843, upload-time = "2025-11-23T12:50:41.519Z" }, - { url = "https://files.pythonhosted.org/packages/44/18/43e36a5a0313a932827ef7b601e673cfec64838458cbb44c452604ac71cc/backports_zstd-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:e005d77a72ba6cd1ad85f46dbc0620922e7b46cb90235c4874d16ab1839b4994", size = 289399, upload-time = "2025-11-23T12:50:42.629Z" }, - { url = "https://files.pythonhosted.org/packages/6a/80/c08720ce3d7c446b5f91c2c894e677b673d87c4e9c7dd6f5c36303baaee8/backports_zstd-1.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d379e9c157e4b18df8852defe1d5da8888ba8ffb4d7d9410428b3245cf8216a", size = 409859, upload-time = "2025-11-23T12:51:04.504Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f1/6220850b09a52cb07854d098307d71d9b330f4d9b49c2fda86c960c7df00/backports_zstd-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79b29d1c66f9bd2bedb87551b303e53f96364a641d30251e9f4e15bf5d84699e", size = 339376, upload-time = "2025-11-23T12:51:05.677Z" }, - { url = "https://files.pythonhosted.org/packages/90/bf/0e6dfe1467dc228916a1715fc75421bafbb5dec4cf306c2395e45d8ef589/backports_zstd-1.1.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:154870d0e0f817159671ffc8b0e39d70ca294542affe6df00d95ef135a2ceef5", size = 420422, upload-time = "2025-11-23T12:51:06.934Z" }, - { url = "https://files.pythonhosted.org/packages/68/9b/85dc1f5dc66c22f691bb60ec739e4be17c53dc670dcfabdf16ff194aec44/backports_zstd-1.1.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f174d8c93bd2682932ecb736e8f03612c162ec6a182c4293a663165f8ffbf5a", size = 393966, upload-time = "2025-11-23T12:51:08.247Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/1a375ea5e1e50741b80f9882c45a13c3b1b051985ae40eae44a34aa4e9e4/backports_zstd-1.1.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b1bf548087476e6ce997c2697072c67193b07e5a36d4eede2b582e7bc7324c2d", size = 413928, upload-time = "2025-11-23T12:51:09.353Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/687326aa8026b28251651add1e87931edbb0bc5bf563776cca6f81f35987/backports_zstd-1.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2a278c276e722399fbc769d1ae318a58578e20086bd09abcffe9af4b1daaf50c", size = 299788, upload-time = "2025-11-23T12:51:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/f7/16/ce8d770a7129540520261694b0e83ec61684ab734f671e4eff652c040a9f/backports_zstd-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:06cef3f3b690221fd55d95cfb0316b914ce72063b99578b309205c9f73ce0bab", size = 409754, upload-time = "2025-11-23T12:51:12.084Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/cbfdd61516d0a8448ad9194e34ffd434ac735ed1a89257f77407dbdbe0f1/backports_zstd-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:55446414f5deda38a4b8de22a0f3f93bacc1a8c4fe2205a0bace0dc55dbf9c90", size = 339289, upload-time = "2025-11-23T12:51:13.407Z" }, - { url = "https://files.pythonhosted.org/packages/97/7f/5e65b583abf56f7e3c54835a707cdd159e47b73f4b8548082debc630baae/backports_zstd-1.1.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f1524c54ec491d80068598ba4328b82c403b99a04c0db6326a5cc8f05565de87", size = 420422, upload-time = "2025-11-23T12:51:14.553Z" }, - { url = "https://files.pythonhosted.org/packages/52/76/14349136c395f94fbd2af663301c2d661da9109fdf74692fe5095808cb9d/backports_zstd-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbcbb091fede530d096e32f3409056c59dc7b5e6cc9bf39131c7961eaaf8f475", size = 393965, upload-time = "2025-11-23T12:51:15.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/5b/fa580166cdf945be0b3ca1dbf8b23bc1370087707fa825b764d7e98b9a3c/backports_zstd-1.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5ac888007be1724efaa565f69ba64ce0c0c0e135d7f22830565399ff76465f1", size = 413929, upload-time = "2025-11-23T12:51:16.75Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f3/650db1631d75c3f6ad46b09dfee2a761b2678b1e2e0004d90258020801c3/backports_zstd-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:08f66e228821a87ba7c74170b6b173cf249eeface98c0951c0da7db680b887d5", size = 299785, upload-time = "2025-11-23T12:51:17.969Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f4/d8/e8426a2abd7bfdafffcc1be07a23890314f199928515937c8ee7e3537157/backports_zstd-1.2.0.tar.gz", hash = "sha256:6c3fc19342db750b52fde793e4440a93575761b1493bb4a1d3b26033d2bd3452", size = 997263, upload-time = "2025-12-06T20:26:39.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/97/01630376854dab8cc6e58819236c46ffc40bc2ad1c5a82b430e0c5b79009/backports_zstd-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68d70396997cbaaf8e571dee93f05c4cec5053ef14a6e165c26ad4aadca6b7ee", size = 435932, upload-time = "2025-12-06T20:24:08.421Z" }, + { url = "https://files.pythonhosted.org/packages/6c/62/a06ddca84e3c0ec45e667a02be5c4a157ab5e1e940d65096a80d409f0557/backports_zstd-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8dc2d15224ea683bbf7ad6aa8eacde40972c2c700e8ff72862cb0663e18ae953", size = 362327, upload-time = "2025-12-06T20:24:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/10/16/1045c674bb09fad1b838098c0b16a88bae9a7bab5e305aac11e55a8c813e/backports_zstd-1.2.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7137114011f380f7e8bd97d92664b5bd71ab5f6e08144f22836263ad45273af2", size = 506230, upload-time = "2025-12-06T20:24:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/8d/ef8a8051374a1948d19c9888d2898d7091314448360a803bffc8474bbf58/backports_zstd-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb922d2f278ec2e62a29121f98e573f622cefa9408dc0462a0e51ac08bca30b8", size = 475845, upload-time = "2025-12-06T20:24:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b9/2c01e970e69f7ea2fa9710373fa4b48d31e677098d04d9b05d01e109feef/backports_zstd-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b72f4fe1764d53c6f8526d53ef566dcbe71daa81b3219fd802b51a153692159a", size = 581469, upload-time = "2025-12-06T20:24:14.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/a2780c37d876badb483815239f2cc9bd863123248e20b2290e3f08355b7e/backports_zstd-1.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:743feb4e13e0f41a22d257bb5d2d4323f0308ecfedfd53db69172e2d9c3e4ba8", size = 641118, upload-time = "2025-12-06T20:24:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/92/88/5f40f9ead71dfb234fb7ab3c7949f2c0aa52eaed3f75115e4e7cfb5528f4/backports_zstd-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:199c41106c05bf12cb665a1d105ce2185da5e190e13b95933420d6fd9cd8bb10", size = 491335, upload-time = "2025-12-06T20:24:17.565Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/4fd0071eb6035d6a412f6c5c2802ebacdbb845569dbb7a1e723c13cc1742/backports_zstd-1.2.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:270f10d14852fd11f85e1199f59fcb9dbcb425d489c678e5b0ea669c091b1cf3", size = 565341, upload-time = "2025-12-06T20:24:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ef/b40a17346294c892d93fa738f48fc145432fa4ff265cf2d1ab3c177f10b1/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c276433f6bbe67a8c71fa78bbfe1d7859ee17f799e6fcff9ac250840e38608d4", size = 481792, upload-time = "2025-12-06T20:24:19.948Z" }, + { url = "https://files.pythonhosted.org/packages/ee/24/7e0d77b17a3ae899a0c3f5ed9be842a8e6134577ea3411d8ff0e3962764d/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:772ad9b56a546fde9c1636dcf525d727330b11c6e34c9af8f879f23b41a8054e", size = 509736, upload-time = "2025-12-06T20:24:21.263Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2a/99f7d247b974de0e5238796e85ba29e49c285a2d8a51c3b6f5b8abd4cd93/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f7fd5d7c8c1c7b26b52b49bf3e392c3c6295658a34a887c587044b37a0b68a3c", size = 585835, upload-time = "2025-12-06T20:24:22.835Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/0eabba1630f4b0674ce6ae79793e8901b0cdb28c83a484b424df29ed66dc/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:18b510dd03a2b7cdda62744802d8c43df7f027f578c4e67f6fa7208d8691db84", size = 563163, upload-time = "2025-12-06T20:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/cc1cd5cbb8982ca156393b8b50698b86efdd0245ba56f3b3b539950061c6/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:26301adc3008c2de40fedf780deb2bceb79471ea89efba37d30768871313f673", size = 631693, upload-time = "2025-12-06T20:24:26.166Z" }, + { url = "https://files.pythonhosted.org/packages/fb/19/01d30f83e87ae4890297f0d2422eb2fad0679c48eb65de8f2c8e131a9345/backports_zstd-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9dc4c9cf3804e6b6cc5aa9aedc63cf81551cc4f6150ea4b248b95de84051317d", size = 495392, upload-time = "2025-12-06T20:24:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/4fb7553901245637ae70d934f5ae719be7207aea3243b254a19f5947b554/backports_zstd-1.2.0-cp310-cp310-win32.whl", hash = "sha256:e456426bf45dd8d818df5ce6b81faaf3961ef8b16834e91cbe2b739346abe9fb", size = 288844, upload-time = "2025-12-06T20:24:28.563Z" }, + { url = "https://files.pythonhosted.org/packages/dd/64/9f8a05ff703f5bdc2b2c9c8e5797299eb2cf4791226a46b2e14489784b4a/backports_zstd-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cf6e2cbe1e637a834e1920ded11e423897a9822d17a0be9486d3f63554f51618", size = 313759, upload-time = "2025-12-06T20:24:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e7/cc45af33a4e4aee365ab76c3f4fcadf984fea221563c2c29c5613cffeaa9/backports_zstd-1.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:4d50bd23c4520e1ccd60af59f8aadc43ce3a481f2793afe01c18a7aa6a518892", size = 288960, upload-time = "2025-12-06T20:24:30.819Z" }, + { url = "https://files.pythonhosted.org/packages/b8/40/f914ee5a00c1f5df9a162efd7130db7ab339b838e6b1613eb2ed7f0594a2/backports_zstd-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47ed63b1c04e06981ac552d107945752d1ffecae98a4bce9c8a627490ce460d", size = 435933, upload-time = "2025-12-06T20:24:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/36/5b/f03eeaee5b17cf88d9f252381f5b8573b1a1c958787af68e9d287c65086a/backports_zstd-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e6433f889edb998abe33f3498c37ddd97b3ce3607eebbc0fed148f8c7c7f2ef", size = 362324, upload-time = "2025-12-06T20:24:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/f8/78/369773911bd9968ca5f4e10ee4232ab6b71cbe45d6e17c78d3399e4a3944/backports_zstd-1.2.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:4cd00e5bfd6d17233809f08d979742a5b9c322162d8daea16f7c3538387b9c64", size = 506229, upload-time = "2025-12-06T20:24:34.364Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/f23872cd114b5352c97bf83a2082427aa08bd22f42461309c23783e82da5/backports_zstd-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8c7239b457f4d51c03634edb0c9b2ebdffc6806f58c0396209f5eb7f8d7642e", size = 475842, upload-time = "2025-12-06T20:24:36.079Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ea/07b6ee0956b522e6a8e0aca97d7b28ed0dc72a7c35a5b77485d2b8d7c4dc/backports_zstd-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75c35e5292d5c5fa879ce3f40428fdb510b11a98801ccf1140690ed7a9c13b3d", size = 581467, upload-time = "2025-12-06T20:24:37.735Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ea/ce04fed217a484ad9f3e8e087dd29c198dbfcb2d4d2c216d044a2a18aea8/backports_zstd-1.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:153e1af06e348f5ed1b104c345880c001824a192536940a8d012d33014b27ecf", size = 641159, upload-time = "2025-12-06T20:24:38.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/8f/b28147869bb8aba7a0b30f05cfec567d90002c4161dabb8315f002709ee3/backports_zstd-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf700fd79758417b1c0b725a56fa485ba15b10ee07ada736ff7e669fddd28b38", size = 491371, upload-time = "2025-12-06T20:24:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/26c7f8bee4cb3e6aae08b04351aa5578d30bac2701197ca2e3cb2b785978/backports_zstd-1.2.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b04638b6adf68f730b04b21ac81fb58eef2ea10f6c221aa653f1009c0afcf67b", size = 565341, upload-time = "2025-12-06T20:24:41.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4b/d1595a7d877e67da6ec6d759d08f5dedaca59d4317c6116b19fd9e3c60bd/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35a445eb01e525ae8dec59fcbabcc373c9ace57f8c10455185038f54a930a039", size = 481793, upload-time = "2025-12-06T20:24:43.036Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/1e1c6a154026bcdd2daecb1abd1d924cb6d274b0f7bae4042f83fb0e97ab/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1923ac203545a18a1b9726f6ae7bed1ab4f8825f0b8f4a32d2795932af3f5322", size = 509738, upload-time = "2025-12-06T20:24:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/40/7e/09a807f3920fa1fe4ae019275d5978168d94fe8615c5bde3f7969760edb7/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97d06ec9b5b21fb59cfa5e716ca1c91f3bac2cd2c3b14e21c3d29fa1b2b0baf9", size = 585823, upload-time = "2025-12-06T20:24:46.001Z" }, + { url = "https://files.pythonhosted.org/packages/aa/14/ef90815a3ad6eabbca59b9cd62013c39acfd38c7cf1f5da31c733520a6d8/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:57e773e295e7d56bd67a2f57071b1c978832566d0f908d7d7aabb16f35401810", size = 563165, upload-time = "2025-12-06T20:24:47.132Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/8918bb085bb2f333d5785cc67918c65e497674de6d53834c1c42233ddde0/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:157950cfd4ed85e03c2557067867e37090796b556c613badfaefcdf2750e95e1", size = 631734, upload-time = "2025-12-06T20:24:48.309Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/a2e494d412728fc04c7e1f40479bac80c505f9eaeafa8048f764104dbfc8/backports_zstd-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a3450d9cf69d76843ea195c1defeff087b68a8a4a3687f0927f870ab594e062", size = 495397, upload-time = "2025-12-06T20:24:49.516Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f9a762ad3e965324a19574c1aa7b39ac35196bc072534efd34b24bec9786/backports_zstd-1.2.0-cp311-cp311-win32.whl", hash = "sha256:77f0e7e71506e12f99927ddea7ab1de5933d47c9af048d05a229246977d89127", size = 288936, upload-time = "2025-12-06T20:24:50.68Z" }, + { url = "https://files.pythonhosted.org/packages/21/95/1d699d9bc9a94ad5b8bc06d1a59246a5adce02668e3773a8c29b1f5a7554/backports_zstd-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:84a0b581408efce8624b887326e0b285fc2e5ba32348b9b6e6775f171fd4926b", size = 313884, upload-time = "2025-12-06T20:24:51.772Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/74b78b9313af6e330b04ae010a98e1d8cc133254c3c53ae2b5e5f4d5ec83/backports_zstd-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:65e7591b20aa803c87a104c0dc9129a984f04adec9b042d88c7a14d1254c9524", size = 289080, upload-time = "2025-12-06T20:24:53.321Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/235dcac25478d60c4e58b6f982b91550b60908dbc07ab42405f818f41794/backports_zstd-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2348b69da19243b7148f69fc60753c27b3efe313dfb29dcb642b4b3a064261", size = 436243, upload-time = "2025-12-06T20:24:54.458Z" }, + { url = "https://files.pythonhosted.org/packages/db/b2/549d1933995ccf4464b29f068f6fdd1e2d9f6abc8ecbcab99dd90d4d28fd/backports_zstd-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f194a3cb53173f8bf8160597d39be16731e576ccf0244e7694e3aeac47e6c85d", size = 362396, upload-time = "2025-12-06T20:24:55.712Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/dc21a59734f2ce145a9a7f2d0016987cedf95598a850a3f4ab6ce73ddea0/backports_zstd-1.2.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:79a18d5d707cd92fc7ce28c4a1a63cfdaf8d19223b3167d2d879042bf1c018ac", size = 506651, upload-time = "2025-12-06T20:24:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/d5/16/12f84de430428f620a6ced01fd2768d2296951d7543b81d971455f39ef75/backports_zstd-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f04bcbb75de26d39de81e7c02a784cb55a706c6ce9665b8df218fa9275193a1a", size = 476474, upload-time = "2025-12-06T20:24:58.136Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8a/980e349fac1e1ba596f440b24901af498399c6e33b83032abbf22fed7d21/backports_zstd-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c36bbd7cc85629d77f9537ad02bc438b3e3f9f1741a43f1cede1027fd9ebfb5", size = 581930, upload-time = "2025-12-06T20:24:59.605Z" }, + { url = "https://files.pythonhosted.org/packages/36/f8/cba3372ca8d777cf5c3e312b35112ff608cf6b0b2f6a813b600b69763495/backports_zstd-1.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:28828f15a7429a7f7570a1465f9b71ccf7f99ea0a6bf786be7c276777f3cdc14", size = 640659, upload-time = "2025-12-06T20:25:01.164Z" }, + { url = "https://files.pythonhosted.org/packages/61/59/c8bd0a5a39770cf7c0d864cbb65ac5df57405ee28a51fd5c11a5fbf1a169/backports_zstd-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d634cba354a3ca61837e5a8d9a6ee19d9d37927ec288f0828437b2620ae83fb", size = 494445, upload-time = "2025-12-06T20:25:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bf/52665f48d449fa1586fb775468e2fa83ebc8e222eb2d18332b3b5f12f933/backports_zstd-1.2.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3624a67d12695d5c32a332bd8cc4c1d45273eba1a4a451a0ecf70f4c3e67dd4f", size = 568897, upload-time = "2025-12-06T20:25:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/e559551d4d206a71ae545fcb690e704dfee141d88984729b0100042e91d3/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:93c9afeee3c60b203644e0a1cc54028283636b4e76ba670c84522584116c1b2e", size = 482506, upload-time = "2025-12-06T20:25:05.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/7440f4c72324c1c455498581faeadc1711cc6728f9d60aa781e6ef939446/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4687dac0a3c5a4c30fbd871ee3be666822f1eb902a7a68ce0d1379f190917850", size = 510079, upload-time = "2025-12-06T20:25:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/248692d5e0960a50eebc982e4e2cbbb3ac0f6200ad81d222d4c01ddd500d/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:dd63ed6c7139cef92b1073be892e92631aa468332570f7230089e93a9449f551", size = 586309, upload-time = "2025-12-06T20:25:08.351Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/ecfb1d8ba18e2c9090898f12b6ea83a9dd59e735021a2c564996c4599024/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:464d5fea68f5b03feabea22a4db4f39622db4ff89dab2df259b3c8665f1f676b", size = 566493, upload-time = "2025-12-06T20:25:09.8Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8d/c81ed0da565f735e8a8f0c3b8c633f9e16bcfdb82ca5cb4d029dac0f1361/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d5b3518f82c518a09067dab4ed1bd79ca542a2c248f7f9418262dc2c4a06ccdb", size = 631120, upload-time = "2025-12-06T20:25:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/9dec5a74249b3af4ea4fd4a5dfb5d70167ea82f96592b3b4e7d340ccfff0/backports_zstd-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d57ce6d62dfb28bd37ad5523678752a5516ec68595fd35559f6c2878edd4de0", size = 498938, upload-time = "2025-12-06T20:25:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/fd/99/ca71a403c79ec4bc419b71cee532de6545af1a0d8e61d0a2b8d70a034e0a/backports_zstd-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e8bedc2372dae260397a99844420e16bb886912d685058d52e1f3533164f67a5", size = 289072, upload-time = "2025-12-06T20:25:13.48Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e4/77fc5813ea35906ac1b71cd284e20c5a6f808f138e2e6a13e9586cd61d1a/backports_zstd-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:4667b30ea5e9f8b505b2042a40c5115660151987ca748b4be07facc757212ff9", size = 314094, upload-time = "2025-12-06T20:25:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/44/e3/133652d59a6731f9180a107812c9d52a2c72be2d80c2fd4f874669592a0c/backports_zstd-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:cd6326f1562435677ab2bf607a44c96bb2a48beb0e14accff45e8c9f0931e9c1", size = 289197, upload-time = "2025-12-06T20:25:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/dd602f484f22d8df9ed71859735dc86e094e90b7d8f51e51d48808f3571e/backports_zstd-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1db671ac69df7cd88057c85a7bc614b94afd74a48faacd1576ad91dd18008f6a", size = 435731, upload-time = "2025-12-06T20:25:17.225Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/b126e05650103f269282e5271a0960e30ac4ce9f192e3ae98e303325011b/backports_zstd-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f6520e555c2b597863b97ecb90ad21857bf044119f643130c29110b55f67c1a", size = 362007, upload-time = "2025-12-06T20:25:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/464e87930aa771da08634846300c13355f7ae07a476c8a30f75631fd1689/backports_zstd-1.2.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9ef28be8bfebf5169cde28f36ebd146a0305569c91e836aed3a3aa79b7bbc58b", size = 505925, upload-time = "2025-12-06T20:25:20.319Z" }, + { url = "https://files.pythonhosted.org/packages/44/08/bda420a2d13be0d6aa8323b735207de46bb01c08575e3a6810e01a20501f/backports_zstd-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9478c04e285da26ce1198d5ec1e43905531049c9e0f74169a39df5876f44643e", size = 476130, upload-time = "2025-12-06T20:25:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/17/d5/fca7eb6e5a12e390ea4437bc6705e18efd70c9966127c3c2fec8188654f1/backports_zstd-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f032452d783876c5f4d81907e42fc6fbe812a9a712c360b497968ea7109bb17", size = 581610, upload-time = "2025-12-06T20:25:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/072fdf5bff7274788b49491a4d039bf0fe2f2f07a9975751d8b70fc14ac6/backports_zstd-1.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5642ae3832fb74817bdabc0d8b8877b109537c3f9ceeb54a6cd855aa0afc3bd9", size = 642454, upload-time = "2025-12-06T20:25:24.28Z" }, + { url = "https://files.pythonhosted.org/packages/2c/74/a7ae8e421ccb779130d64745d3191daf6da02f37bf7cf099dc10d688d14e/backports_zstd-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e7340a7fb7bac7607382554b138ae8dbffda0b8af72ce5c639fb86b49a3b2e4", size = 491079, upload-time = "2025-12-06T20:25:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/25/26/450b23bad6035f0f3dc8e1cf3729e31a10ce1821a7b6d3bf8555ba818a46/backports_zstd-1.2.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b72e343b1b2927fc047054450de8738bc64c268e93fabc8228d963690eaf348e", size = 566373, upload-time = "2025-12-06T20:25:27.034Z" }, + { url = "https://files.pythonhosted.org/packages/c0/42/8161164fb26ac1a656f5fb5e3aa3aa9dcf4d06f6d9553fc596fa6f0ae3ad/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3f2a3f898dd1eaba5c8f0ea0e1bd23d993ed86ea7d4c12bf7a0743158d123d6", size = 481888, upload-time = "2025-12-06T20:25:28.198Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2c/5c4f9a54c7c708f38cdb0670804d62237e9cacd1a6ff567f8c5fb8ef5d1a/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:afb95bc88d7623eeda2c0c8fe0388ac8838fa5a09ddbd7dfa72b283de392024b", size = 509480, upload-time = "2025-12-06T20:25:29.438Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/f7a86785fde290b3280adeb81c577a96323d4b661b9befa0d990aaa67a86/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:59ddb84b8bd46a4a297cdf92601aaa9f59881c59f4a402a021173d6bb8bc367a", size = 586040, upload-time = "2025-12-06T20:25:30.598Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/11c709c72abeef82c5ef752718c10a3fde0fba8258c069d717de33d366f7/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d3d0d54ef711ea5ffa4e2eebfb70784295eb517bd7ac64545a142ad35c5b02ba", size = 564125, upload-time = "2025-12-06T20:25:31.772Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ec/2ea033ceeca0808d830c3fb1d8ced1b6d2e5c4540ed8bdf66e0ec99180af/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5c170f1329e90614f2d51f6f4622f6f775f51b92b7bc7801fa093b97db6cfc95", size = 632819, upload-time = "2025-12-06T20:25:33.027Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3a/1469312ce7b1c6d98c788b500df01d61468d17bfd58df21266a7160112fe/backports_zstd-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:49c675121210ee23ea9c9b28ad15822e1b0f9182df733f0e1a10a5385f628701", size = 495375, upload-time = "2025-12-06T20:25:34.362Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/27ef9e6a169808fc6777fb58fb55991bdc9ec22eee1aaecaf076be91986b/backports_zstd-1.2.0-cp313-cp313-win32.whl", hash = "sha256:0724fb2958eb2ccc100c9f49315d856a88b5deb985c62953876a78ecb46027ba", size = 288717, upload-time = "2025-12-06T20:25:35.565Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/bfe0c470e2cfd0bbc274ca484e7a061f2d505c28df2479c52ed1dcce4fea/backports_zstd-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b80b715170fc590d464d3342757978702ed4b6b41a3b3a0a5dbb46a89f4ccebe", size = 313927, upload-time = "2025-12-06T20:25:36.691Z" }, + { url = "https://files.pythonhosted.org/packages/16/0d/cf6b22e4ca8cfa850061baa89664437b2da65698b2a8a02eb67de9d6b69e/backports_zstd-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c09f5e985142fc04581c12e635dfd8cdc64b2d595479c1213f0b4db7ee37e3cb", size = 288947, upload-time = "2025-12-06T20:25:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/285bccc9fddb1b1d7fa379b42407f3e88359db05a6955fbe02ef4d5ed6ce/backports_zstd-1.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:21c68b9d358f1062c5c8baad946e306b54380dcbe2b12d00fdebc42533b5a499", size = 436423, upload-time = "2025-12-06T20:25:39.048Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/bba33f68cfc57a2885bbd1a1ffc9405cb461b12db5083ca63b15074bfa6d/backports_zstd-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4c3508074a30e3309bcc1bfdfe6cbd6bd3d64567788d3c6e15b1594e63bef276", size = 362704, upload-time = "2025-12-06T20:25:40.232Z" }, + { url = "https://files.pythonhosted.org/packages/42/62/9233d99c1be673188afde322aecc467653023185077064dd3eac18678e22/backports_zstd-1.2.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9a0e5f6aebd1ff3c75b26d3e3ac3140996b9f0883b95847fad57992be06fe5d2", size = 507870, upload-time = "2025-12-06T20:25:41.364Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/089867ef455f6311caa9224e68020cabcbfc3f8759cfd19931b06ba7bb8d/backports_zstd-1.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b12f415fe62f1e9f7196ce9eaedc699547c38cc263e64b6939b610b2a29a200", size = 475770, upload-time = "2025-12-06T20:25:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/43/d5/8615151ea4bcf5d2dca0755bfa7cad97b7cb8dbd1c9c2e1da57081f1d8a6/backports_zstd-1.2.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00066e7c3df700fe0df66aa197dcbd3d691d55c16f6179c3acd87a6ca22e7993", size = 581191, upload-time = "2025-12-06T20:25:44.156Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/eb6e7019db30622d2a31faa0e608fc3dc29e336b4a855bd5b0e78ac0943e/backports_zstd-1.2.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c56bdb8e57563d726ce46225011801ff748018cbeff5175576f8a46868e0f706", size = 640185, upload-time = "2025-12-06T20:25:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/d6/64/ebd64f84875f7feb83005bfc3e7352700d26f5396e7e5e494681af18fe18/backports_zstd-1.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f499ec4944c223814be97627d0f1b65c2474ba981e1f21ec8b541f2dff5f960", size = 495073, upload-time = "2025-12-06T20:25:46.546Z" }, + { url = "https://files.pythonhosted.org/packages/45/a7/8f44bbd2a78855680c24e7f90cad69c0c70650f345c4cf13bc7a6dd56231/backports_zstd-1.2.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0957bc3eb84b4a537388c7a62ed055d61c2246c11babf4cbaafb3b5eda0a9c", size = 570664, upload-time = "2025-12-06T20:25:47.731Z" }, + { url = "https://files.pythonhosted.org/packages/85/e3/476a8b3ac1d7d1b41442bf634603d0beb392d9df2a1439f9543312b67bc3/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e38b22cb32bd9010ad65c0b75a2934448adfdd6bba4387b0f9bacce11e051716", size = 482252, upload-time = "2025-12-06T20:25:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2f/cebac7f4dc977da1dfbef4004dae6ae12e31f6978db34af530425568dea7/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9af83bbd56f785dffc9a669918e7e7b7f96b0e32fd8877ff90e445ca531463ae", size = 511631, upload-time = "2025-12-06T20:25:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/1e9a135af0141d0e0eabb21491f0f058e73ddb4de356ca174406d60d1dd6/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e3ddf78b1b58291616df13844d5a0e6027b0ce299b15d7d3be2a93b974b7b5f6", size = 585764, upload-time = "2025-12-06T20:25:51.998Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/e9b29c3d7c4d13e046876fb81b7564a85463d6ab65c10f7fc42a7c658042/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:93332bf758954da70b1803ccc73697c7cdec475e4ec04286ff8c568f3786c398", size = 568581, upload-time = "2025-12-06T20:25:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b5/e2f2f68c204743a9060e50f6910fd7ae1b484250460fa85e843292e8e8d8/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:8f5ba6a97712794ab9c284382a5faefd31ebcb84fa55cb4621e948264dbc6b97", size = 630790, upload-time = "2025-12-06T20:25:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/78/ff/87fc0e498e6c4ad475da178a4e4c81fbfaf19b043ca586a63068ad704f49/backports_zstd-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9d541c0c1992ac25bfd836a1e77cf0e70f18d5505aeeebc56c2cf06ffbc209f5", size = 499701, upload-time = "2025-12-06T20:25:56.265Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2e/e10d1b10f087094a108f542b91b44efe928800b6ecd8f1619c9967b6da5b/backports_zstd-1.2.0-cp313-cp313t-win32.whl", hash = "sha256:6c2bc4f7154d5c166912c5b7ee6cbb3a921726d1c2cee7c0f1bfaae736f7c250", size = 289660, upload-time = "2025-12-06T20:25:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ae/68e5bca7f9990c45c7b1940deccfa4a106c956c5ddd63393332372dad131/backports_zstd-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b7b83331878041a259ec0b384bee7dbfd486b2a2579cd730aca718897d045d7", size = 315026, upload-time = "2025-12-06T20:25:58.952Z" }, + { url = "https://files.pythonhosted.org/packages/e4/33/a519b4da2015069fb36cded5181ff078ecceb852861b675e2c79547ad10d/backports_zstd-1.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a884be79cd0897436e1e06566d0b6bcad2360afca8e8e27fb19422ba0cca4d7a", size = 289583, upload-time = "2025-12-06T20:26:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/0be0f44bfd5a77b6dc476eae791bb2847f786bef717aab510b0764aba2f9/backports_zstd-1.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f94f434e2265c067a7e6e2ea50f93e709421f2c9e4a2458a80284065a79caefd", size = 410041, upload-time = "2025-12-06T20:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/1d2390341fb97e9fa9c3242dce6825646bd6f47d96ca862bf070dce0c943/backports_zstd-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0d9e4d77f03524b59bc2d8d9636e5d808e50ef0d20c56f0ab2ab8ee00b6a367a", size = 339556, upload-time = "2025-12-06T20:26:24.718Z" }, + { url = "https://files.pythonhosted.org/packages/5f/21/36a2a17f5cd360ddd89bc6d24d2cfb1f6b1e4051fe70da9e172697763d7d/backports_zstd-1.2.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5d8014aeaec1f47f3209bd5e9e29282fcafa7b9076f89cc342a5dab3f298fdec", size = 420605, upload-time = "2025-12-06T20:26:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/ee54f99fda973cf18dcb5ef7fb481449b1e5770af4f449ac06af77c995d2/backports_zstd-1.2.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35b8390fde5644a067a66cbd6f1b1293f48264f7bd867f0148b09d4f206005bf", size = 394149, upload-time = "2025-12-06T20:26:27.57Z" }, + { url = "https://files.pythonhosted.org/packages/36/d6/1e41a5469606fbe100b4841f03c42275a1b114fa02921cbdedb0aadeaa00/backports_zstd-1.2.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb3b0e175170422b1d095709356cb688b664d381f3dba424ae5107990ca913ee", size = 414113, upload-time = "2025-12-06T20:26:29.072Z" }, + { url = "https://files.pythonhosted.org/packages/46/68/26c9802339a885f567f1c7bbfa5d5b786545e5bb754ba385f81dd6d2ccc1/backports_zstd-1.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5f7e28d44e322c16aaf8973ce3c062105b6d88fe2b4f4611b40e410176a4fd40", size = 299966, upload-time = "2025-12-06T20:26:30.333Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/2b7b0e1dcd165cf0e0fc792b74138489bfb90d66d7ded86f7f7e91f6764c/backports_zstd-1.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c3a1748eaac8fd1c862d3e16c6beb023f118a82d7230a32d33f6ce65752a2d6", size = 409938, upload-time = "2025-12-06T20:26:31.55Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/c91b6a4681eb8f13c7919ce551d4b5364e9fd6f07e770e4e01ca2c0b1f92/backports_zstd-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9e126bd32c97b1f4717d30530a3762c1f9a85d5d629fdc2ad210e6427fd6849c", size = 339472, upload-time = "2025-12-06T20:26:32.765Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e6/abb1b8e6e5c9dfb8cbf05669745de81273b46fef5bafee00fb1698c75ce8/backports_zstd-1.2.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:e4524beed644c4074ff017e96afc91c5e98064f40680fa859bddee5974641805", size = 420603, upload-time = "2025-12-06T20:26:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ca206b678cdbd2eca56aa2ce49996f6d6cd21db840efa6e2e6f73d4cb7db/backports_zstd-1.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72bc2b03590d66d8706e4717da25cc6c192f5a6bfc3f6148f671f79e73afd4e8", size = 394149, upload-time = "2025-12-06T20:26:35.563Z" }, + { url = "https://files.pythonhosted.org/packages/50/c2/ba7bcfe28dc3f8e8848419757883c8a0c7a4263dcd5d3988dd7f49818ca8/backports_zstd-1.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89afe7d6e86bedbf2bac083beb096732a1e92025a5efa9c972941a6140994485", size = 414114, upload-time = "2025-12-06T20:26:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/71/ad/d5e8a3b28150e4f310999ef26db1e6b5f3bbb899c07d88ebd910954fcaf2/backports_zstd-1.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f00066de6ffd72c653b43afb9aaa36969fd0e2c91f66adb45a11f73e6423263a", size = 299968, upload-time = "2025-12-06T20:26:38.382Z" }, ] [[package]] @@ -155,7 +165,7 @@ toml = [ [[package]] name = "black" -version = "25.11.0" +version = "25.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -167,29 +177,34 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, - { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, - { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, - { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, - { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, - { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, - { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, - { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, - { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, - { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d5/8d3145999d380e5d09bb00b0f7024bf0a8ccb5c07b5648e9295f02ec1d98/black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8", size = 1895720, upload-time = "2025-12-08T01:46:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/06/97/7acc85c4add41098f4f076b21e3e4e383ad6ed0a3da26b2c89627241fc11/black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a", size = 1727193, upload-time = "2025-12-08T01:52:26.674Z" }, + { url = "https://files.pythonhosted.org/packages/24/f0/fdf0eb8ba907ddeb62255227d29d349e8256ef03558fbcadfbc26ecfe3b2/black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea", size = 1774506, upload-time = "2025-12-08T01:46:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f5/9203a78efe00d13336786b133c6180a9303d46908a9aa72d1104ca214222/black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f", size = 1416085, upload-time = "2025-12-08T01:46:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/7a6090e6b081c3316282c05c546e76affdce7bf7a3b7d2c3a2a69438bd01/black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da", size = 1226038, upload-time = "2025-12-08T01:45:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, + { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, ] [[package]] @@ -221,11 +236,11 @@ filecache = [ [[package]] name = "cachetools" -version = "6.2.2" +version = "6.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] [[package]] @@ -450,101 +465,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" }, - { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" }, - { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" }, - { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, - { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, - { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, - { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, - { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, - { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, - { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, - { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, - { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, - { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, - { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, - { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, - { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, - { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, - { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, - { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, - { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, - { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, - { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, - { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, ] [package.optional-dependencies] @@ -691,11 +706,11 @@ wheels = [ [[package]] name = "docutils" -version = "0.22.3" +version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] @@ -721,11 +736,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, ] [[package]] @@ -751,7 +766,7 @@ wheels = [ [[package]] name = "hatch" -version = "1.16.1" +version = "1.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-zstd", marker = "python_full_version < '3.14'" }, @@ -772,9 +787,9 @@ dependencies = [ { name = "uv" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/03/7fb64d1a85c84843890007b545dd64702ec3ace972df6fc3655badc1f152/hatch-1.16.1.tar.gz", hash = "sha256:87df6a17a0f8dc6f9a8ae459b742cb32ccc844a79634f9072797a2e38af1280f", size = 5215833, upload-time = "2025-11-27T20:06:41.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c1/8598996a6f264d430c530799dc65fb13942fb29092e35505039a5f2fb5dc/hatch-1.16.2.tar.gz", hash = "sha256:f288938da85b4b90e47d94788e19e9976dcd6fd53b48343ea251a2a37256a980", size = 5216569, upload-time = "2025-12-06T19:18:12.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/39/42d25fed23c4fd4eb3c4ff3f0a7f547ed0d4ad6a1efb0105da0f43768141/hatch-1.16.1-py3-none-any.whl", hash = "sha256:c7c90d122f8fca602d10c9f2176027f935cc6071710d4aec2d94490f6c5ef697", size = 140600, upload-time = "2025-11-27T20:06:39.547Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl", hash = "sha256:827eaf9813c63119f172b85975c5c27110a2306b07e5304c9d38527b0239052a", size = 140658, upload-time = "2025-12-06T19:18:10.573Z" }, ] [[package]] @@ -865,14 +880,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -987,14 +1002,14 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] @@ -1038,75 +1053,75 @@ wheels = [ [[package]] name = "librt" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/1a/38eea84b96d964bf9756d16f20534c4442103345719f600b1b267233b7bb/librt-0.7.2.tar.gz", hash = "sha256:48aa0f311bdf90ec9a63e3669b6aff04967f24f2f67fe9372c570a21dc9ae873", size = 144193, upload-time = "2025-12-06T12:04:43.486Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/6d/c03e987d605fa690845847c4d334e20cf66a071f90d42df07b82aa863200/librt-0.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0090f146caa593f47e641307bd0bef778b76629b1d7a5bec95d3a83ed49d49de", size = 54706, upload-time = "2025-12-06T12:02:59.756Z" }, - { url = "https://files.pythonhosted.org/packages/8f/17/9d2e52b9aabdb0492b2659f8c3de6c5b7eb834909d6461284a65decc1768/librt-0.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c44321bc013cf4b41169e463a2c441412497cea44dbf79eee0ccad8104d05b7b", size = 56660, upload-time = "2025-12-06T12:03:00.994Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fd/7c517ce71f8df2cacc981496665f18f54d9673538190ed21a48fab658ea5/librt-0.7.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8913d92224da3e0ef54e40cdc36f1c0789f375349aa36f7fd44c89dfda1e6d24", size = 161045, upload-time = "2025-12-06T12:03:02.468Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/054199fb94beb488c560450012fdfe8e69ae3ed7a78f5f2e1a44053cb26a/librt-0.7.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f968b951f0713b15ad56090c5499bc63e4718e7636d698e1e1fc2eb66c855f97", size = 169534, upload-time = "2025-12-06T12:03:03.785Z" }, - { url = "https://files.pythonhosted.org/packages/cf/66/584bb0f39f98101a93ba1df115237465c6a20f53047a49d0535f5609f01f/librt-0.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e8801d41dcfbb76407daa5e35e69ebe7b0fc826b7c63d462cbbab530b5672b", size = 183277, upload-time = "2025-12-06T12:03:05.315Z" }, - { url = "https://files.pythonhosted.org/packages/6f/17/4ce7d95fa3e7eeda3134921b4e185c1ed190f027c2a8008fc8a60d3e70b3/librt-0.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9672ee71a08c5b1cb5bb92fc5cc07f88c947716ff3c6b8c3bc0f57ee7ddc12fa", size = 179045, upload-time = "2025-12-06T12:03:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c8/bfb44d75eaaae5d9bdebc08e156faf535adc942704463673384ef52cb869/librt-0.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9786b621b5c7e6e2aaab0cacf118c1c3af5f70b9c0e3fe614734b1d9fbc37cd3", size = 173519, upload-time = "2025-12-06T12:03:08.504Z" }, - { url = "https://files.pythonhosted.org/packages/a3/6b/748d14d54c5b2b1bfc80f208d25b265b6c6d23cdb05f70a981da2585b66a/librt-0.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:332bd6505e345c0d92ad5ede7419bdd2c96ad7526681be5feb2bb26667819c4f", size = 193591, upload-time = "2025-12-06T12:03:09.789Z" }, - { url = "https://files.pythonhosted.org/packages/17/46/ae2d81b8cdc285a0e7f6c7868c258fdfcd1b468eaee99590a4edacf21424/librt-0.7.2-cp310-cp310-win32.whl", hash = "sha256:0ca4ff852be76094074bede6fcd1fc75374962ec365aceb396fa7aa3bc733c12", size = 47204, upload-time = "2025-12-06T12:03:11.313Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b9/99ecc8f07f9979cd3e6535727b17465ca478ab98b23eed49bb1e66ed8fd2/librt-0.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:dd2b75815270534c62e203ee5755ae1f66540ce4ee08432d4b1e623ddb2fa175", size = 54372, upload-time = "2025-12-06T12:03:12.459Z" }, - { url = "https://files.pythonhosted.org/packages/aa/66/a425c03e074915306c19b791622d7f2f1940aa3db2fee15671a420fb0849/librt-0.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f8f02d40621f55c659ff1ed7ea91320f8bc16e75fe67f822445cd0e9b5fa1d1", size = 54712, upload-time = "2025-12-06T12:03:13.937Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fc/25a895beec5619b65e156ff913004af355322bb83e424fec3633bef281cf/librt-0.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0bc8425c7f9e9bfc16fae651b56b402b11e25c223a90353fb71fa47ed3e1c048", size = 56659, upload-time = "2025-12-06T12:03:15.13Z" }, - { url = "https://files.pythonhosted.org/packages/0b/6c/dd3ed2b6572efe6a2da6b48c8fb94fa965e1a70da67716060de2909e6526/librt-0.7.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f9a8a6e3cea9c01d2d9c55cf81ab68580b10d01c54b82cab89e85ba036e1d272", size = 161702, upload-time = "2025-12-06T12:03:16.652Z" }, - { url = "https://files.pythonhosted.org/packages/55/47/397b6735d7027e1ee1b1db91d63d354151f797af8d9e3c8f6635e847dbbe/librt-0.7.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de0aceb7d19f6dd4aa6594be45f82af19c74bd0fcf2fa2d42c116d25826f1625", size = 171039, upload-time = "2025-12-06T12:03:18.344Z" }, - { url = "https://files.pythonhosted.org/packages/96/21/c0f0a0397a084f4e0647d1365cb5d1060725d1b77645b7d5a8cd96b22c96/librt-0.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d29bb29aba2a849ea8255744655b359ce420ab55018c31a9b58c103415e47918", size = 184718, upload-time = "2025-12-06T12:03:19.696Z" }, - { url = "https://files.pythonhosted.org/packages/6d/36/d8fd4c8bdad854ba258fff9f2573f4fe6c662bee38e53f1bf64c7b4c921d/librt-0.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f172088974eac0101ecbe460d89411c945fa57601e4fc3dc461e718991322e00", size = 180732, upload-time = "2025-12-06T12:03:21.211Z" }, - { url = "https://files.pythonhosted.org/packages/f9/83/5293405e85142a03b4c0c15147d31616035c0d8aae9e20308d2facba7d6f/librt-0.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab4ca61a3b774d3b1886b26f7cc295e75a42ebc26c7a1a04e11c427e5313922f", size = 174564, upload-time = "2025-12-06T12:03:22.49Z" }, - { url = "https://files.pythonhosted.org/packages/ff/92/7be3ad4403d5b0e955abefc2508a7f942057f70d0edc197dafd1b1e6c2a0/librt-0.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d891fb657a14d8d77e3b565332e064fbcd67741e99043634e5b7cbded88d9d8e", size = 195247, upload-time = "2025-12-06T12:03:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/c18575def44c6fcc1d0d39e249fc7df68b596618d675ff9d3fb1138e4fac/librt-0.7.2-cp311-cp311-win32.whl", hash = "sha256:2272e1a4752ad0b9f59793f63ffce06178fbe15a1fd4d2d8ad9ea2fe026d9912", size = 47516, upload-time = "2025-12-06T12:03:25.299Z" }, - { url = "https://files.pythonhosted.org/packages/bf/75/92cb954c36ea687f3cbf418ee7b8634c97d48e45e3b28c8031e87ee0b8f4/librt-0.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:eab548b8c771a1846d328a01e83c14ed0414853bf9a91fe7c692f74de513238f", size = 54698, upload-time = "2025-12-06T12:03:26.422Z" }, - { url = "https://files.pythonhosted.org/packages/26/8e/b1258d866521e5ae70dc91e98062ae86fc64270c9fc4256cafe0307a0717/librt-0.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:0259a726416369e22306177be3404cc29b88fc806d31100802c816fd29f58873", size = 48148, upload-time = "2025-12-06T12:03:27.884Z" }, - { url = "https://files.pythonhosted.org/packages/50/7a/7b41d6f27bdbbd4146ee3905b5f65ab400d5562819173c3ffdea8349dc86/librt-0.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d56630bd5793ca860f148cfa6d79a81b3d9c7d5544991c906a8f412eecce63", size = 55693, upload-time = "2025-12-06T12:03:28.995Z" }, - { url = "https://files.pythonhosted.org/packages/4f/fb/c6f0ac08e479cf18e1dab20f7e983494a4e08570f05caea8cdbdf78f9fee/librt-0.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4076beec27478116ff276731daf676ecd03ceae03fabdefdca400f7e837f477a", size = 57123, upload-time = "2025-12-06T12:03:30.441Z" }, - { url = "https://files.pythonhosted.org/packages/74/99/d0ab1adcabdc5b12207c3132f13bb32a59f5bebc3a3b5f9baf3c17f29806/librt-0.7.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7698a3b75f0aa004fa089410b44088628851b3c62c9044822c61a8367fc8caea", size = 165336, upload-time = "2025-12-06T12:03:31.683Z" }, - { url = "https://files.pythonhosted.org/packages/de/27/3a6f0d3541db87c4ffdffdbf76b200855f4e567f4e9ecc09ad449c0432d9/librt-0.7.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e706fdfef8692ee82ac5464c822800d99b436511a9bba402a88e878751b342a9", size = 174235, upload-time = "2025-12-06T12:03:32.991Z" }, - { url = "https://files.pythonhosted.org/packages/90/f3/ae8c1f537741dbdf13b1112f611bc19053970e42bc6da34801f0624131df/librt-0.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39d2b8df134910a2c58d91fbf50cd6ea0b815a50fcdf45de1e21af0a10fcb606", size = 189020, upload-time = "2025-12-06T12:03:34.269Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b7/f18d3b0561d94f914ef9c5a7790ba76038254d4eece3db0b7b807a49c9df/librt-0.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:035c5f2f4bd96326f4528ce48bd60ed19ae35f0c000540971aa597a441e83509", size = 183984, upload-time = "2025-12-06T12:03:35.637Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8c/c271e3e5d0196d03a8eea27d6f132ed7df586327a5c3734f095d17b86a3b/librt-0.7.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14798167e2be3cb8202c9617d90d5e4b2b50a92a9c30f8aceb672e12cf26abbf", size = 177600, upload-time = "2025-12-06T12:03:37.03Z" }, - { url = "https://files.pythonhosted.org/packages/42/81/ef1161d171cb274b8881ae138566739c9162189f58adffa3bc2f50ab2cd0/librt-0.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6b564c8e9e768fe79651d626917b4b3d10b3d587779eda2231e235b64caab41", size = 199283, upload-time = "2025-12-06T12:03:38.377Z" }, - { url = "https://files.pythonhosted.org/packages/3e/30/88e7e1c00dfd8d48ebb1dac2498080aa62e8ba7226b9711347cab48b60f1/librt-0.7.2-cp312-cp312-win32.whl", hash = "sha256:605c7bbc94aa30288d33d2ade86d3a70c939efa01f3e64d98d72a72466d43161", size = 47884, upload-time = "2025-12-06T12:03:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/db/2d/8d3d5fb63a59316eb0033c4f1ba153de8c118db4274810231b09fc42241c/librt-0.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:a48f4c5d3d12eced3462d135ecfe0c4e2a143e64161a471b3f3c1491330fcd74", size = 54977, upload-time = "2025-12-06T12:03:40.994Z" }, - { url = "https://files.pythonhosted.org/packages/1c/57/dfd1406ae8af9dbb73d3b2a330107d0205e09a3d1acf2e89cc08d6f7ef1c/librt-0.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:0cbe93690e07c9d4ac76bed107e1be8a612dd6fbc94e21a17a5cff002f5f55d5", size = 48343, upload-time = "2025-12-06T12:03:42.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/80/f3696ba44401c90ab2cc619cbd663ed4f3faf25e86df289ced7b4abf96c2/librt-0.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b8fdc5e6eb9698ed66bb652f18fa637853fd03b016864bed098f1a28a8d129d", size = 55742, upload-time = "2025-12-06T12:03:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/d4/a5/36880025707514d6207f4044adbaef08fca3d54fa0b9294ae38553db8a95/librt-0.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:66d0f0de87033ab7e54f48bd46c042d047ecc3d4e4d5b7b1071e934f34d97054", size = 57170, upload-time = "2025-12-06T12:03:44.402Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/c33f6a51cc6f8f67fdb872bc8e0c07e954962b3fc57ca791367b7b72433d/librt-0.7.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9da65ed19f6c7c4bbebd7acb37d4dbb95943792b51a74bc96d35673270853e16", size = 165840, upload-time = "2025-12-06T12:03:45.568Z" }, - { url = "https://files.pythonhosted.org/packages/5c/b1/173e34dc7b05a34414905ba8658618a740182fc13b8b613f472dbc94ab25/librt-0.7.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eeb76e18c2adac6bcc709ba7f728acca2d42baf0c7a3b9eba392bab84d591961", size = 174824, upload-time = "2025-12-06T12:03:47.062Z" }, - { url = "https://files.pythonhosted.org/packages/69/58/edff7823c9bb89aa2f41b3156a2928d8ebf8f3396b119343ab8236339f91/librt-0.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5d5f8f617fc3db80864f7353f43db69d9282bf9cd74c7e6cf5be1a7e5d5a83f", size = 189613, upload-time = "2025-12-06T12:03:48.67Z" }, - { url = "https://files.pythonhosted.org/packages/5f/64/2277eeaea081e826f155b1de9f0809b007cc7e606faf17befb5c77d54bda/librt-0.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cae1b429f9077254622d7d12ade5d04a6b326b2ff456d032fa3fa653ef994979", size = 184584, upload-time = "2025-12-06T12:03:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/31/c1/93075ab0337e83ed2a3d23837dabf7df1e2b6f8ea22e7701d6adbb8f32ce/librt-0.7.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:edd20b266055b41ccee667b9373b3eff9d77b8e0890fd26a469c89ef48b29bf0", size = 178268, upload-time = "2025-12-06T12:03:51.255Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/bee97a425ccdf05282a4706dcc83a6d440da67559cc7ec37fa8b7caa7e2d/librt-0.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cf748211b5782fb9e85945d7ffdef9587bf303344e2ad3e65dee55b44b1c8ac1", size = 199853, upload-time = "2025-12-06T12:03:52.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/ea/eb8658a1979d27d3e5cdbff0666a1633ef9b7d5e2be49cb90c649fb66ba4/librt-0.7.2-cp313-cp313-win32.whl", hash = "sha256:c4fefe752dcf30564b031e85e6cbc70d82685e52fbbfffc6fab275a47b5c3de7", size = 47939, upload-time = "2025-12-06T12:03:54.213Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/a96c6811e259efc7d2347f572b27d7ddb2976c8db9408a545291c8c8b25c/librt-0.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:65cd928b7e0c1142235e54e4b615a0a7f4ad046d1d4cbdd454c311bafca97aed", size = 54968, upload-time = "2025-12-06T12:03:55.364Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9f/bf7eaa2cbf27d88f7e6cca6c7651192315cc834ba6c20b8afa1228aad3c6/librt-0.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:10d6d5d52026e44ddd0f638e822a5d451df0d5b6701cb5112362a3a9f4b00229", size = 48355, upload-time = "2025-12-06T12:03:56.504Z" }, - { url = "https://files.pythonhosted.org/packages/c6/42/97a7d52d3c1ba9503cd9c973105f65c506b2b491743fb0e613d1683feac8/librt-0.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0baabd8daa4339f6cbffada3c66795722c37880ce768de83c7cba379d469ee3b", size = 55177, upload-time = "2025-12-06T12:03:57.664Z" }, - { url = "https://files.pythonhosted.org/packages/83/1e/44ac0089b506f7b5880c5b090f1315099b77e6854feb9002cf0e8089b318/librt-0.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:462d9672a4ade935d78c70713847bcba643bf4d94c013fdf29ea5f153bb15922", size = 56883, upload-time = "2025-12-06T12:03:59.362Z" }, - { url = "https://files.pythonhosted.org/packages/b7/1a/a0b8ae021524090322b07713f1fbf491c78945d878ee3aa17c81efddd8ee/librt-0.7.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838b16343fc4ed6869edb3ed9dc89c4bc9b113b1c6028592bede4a93ad360aa4", size = 163711, upload-time = "2025-12-06T12:04:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/9f/61/c80a89ad8be77adb592983f3fdcec273ba90384f896cbaf851ff6a61bf41/librt-0.7.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b6ee74bfa7055c07e0acb56226efd49687488486db8fcfdea5da4cf25323a91", size = 172472, upload-time = "2025-12-06T12:04:01.887Z" }, - { url = "https://files.pythonhosted.org/packages/68/20/315e95f1c19004e2b63e913aae967b1040e900be74f4ea65f4ccaa87ec19/librt-0.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5e3502a543b9b3f906f6d4e88582b7ba13320897e19c60d7c098fa9fda1611f", size = 186805, upload-time = "2025-12-06T12:04:03.185Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/97407f07fbd1cb323870e80b016733737f3042a853298455aa5d2d9c0b5a/librt-0.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0f330d6af5bcfba339690694bf7c4aedabfa3dd40b17212a2b94a417962ccf", size = 181819, upload-time = "2025-12-06T12:04:04.783Z" }, - { url = "https://files.pythonhosted.org/packages/c8/33/45c99e956279f0fe318cf41fc68568958709a4de67668fc51b59529862ea/librt-0.7.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:610a25e8239836fe8eff92628602db13dca5d867e868503239c37f3809b3ce9a", size = 175602, upload-time = "2025-12-06T12:04:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/26/85/31503175c285dc17748b33ec2870073e4d147248dc6fbaeab0f8775d59fe/librt-0.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98854ffd0dd6fd64b8a4be4973593746038152e6c239251de908b5a176d8f64a", size = 196497, upload-time = "2025-12-06T12:04:07.641Z" }, - { url = "https://files.pythonhosted.org/packages/bf/35/fba5be06dee862ee6fa532ded2adf05f9249d6cc243489d8ea8c6edfe52b/librt-0.7.2-cp314-cp314-win32.whl", hash = "sha256:879f789b22e9534df279a6cd3af12d26f8fd96785c47db0d2508304cfc6fd7d9", size = 44681, upload-time = "2025-12-06T12:04:08.882Z" }, - { url = "https://files.pythonhosted.org/packages/6d/df/3706f58e2cf8b6827648b70eaeb00bff37602f2fc5f6678686bff1b78881/librt-0.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:cba3ee432767960ce1e908c67c1fa136831c25ac3705e1e188e63ddaf1b46a06", size = 51693, upload-time = "2025-12-06T12:04:10.006Z" }, - { url = "https://files.pythonhosted.org/packages/49/39/516a859d00c4c33ef37c8f1b0e709685587028d904038ca7d7c317fc0802/librt-0.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:d775e5de996105c9a85136c18bce94204f57021af77a913644e8f9b17733a917", size = 44665, upload-time = "2025-12-06T12:04:11.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8b/624c71d15c5507b070d8cb76d1dade630b144276bb05654b47fb24d7d616/librt-0.7.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:7fecc4dcc74e0c97ca36435048e3392ee6aa2ae3e77c285394192f9ad1e1a283", size = 57353, upload-time = "2025-12-06T12:04:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3a/fe16e8116949325fd584f388f69ec42aaf0711869137601c90f1fd6804dc/librt-0.7.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d643941996b678699fed64271d02439fe23d31d8dee45f0e0b02c81ee77a4d79", size = 59217, upload-time = "2025-12-06T12:04:14.768Z" }, - { url = "https://files.pythonhosted.org/packages/36/fd/74b8db240a27493247cb1b7185fc098f98984fba128513f56a395195c64a/librt-0.7.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dcefbd09a5db038693d22adc1962111d4c2df0b838fde2f3a61fceec9953b9c5", size = 183860, upload-time = "2025-12-06T12:04:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/e7/05/67485dee0b3c134df80426d6dceac1b4259fc1a428e1e0e9211f1a6798b9/librt-0.7.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11445c0460d4522c5959f7770015cdcd7dd025ac2c095c50b78e27878f9cab15", size = 194592, upload-time = "2025-12-06T12:04:17.496Z" }, - { url = "https://files.pythonhosted.org/packages/a2/27/eb4a6e3f88fefae026bda0e80f1a270c942c8f5ff471ebedc964e0899d41/librt-0.7.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c10ae62472a03dc8db52d5dca4a9af5d0935899cf8c550565a39645bf7735d87", size = 206760, upload-time = "2025-12-06T12:04:18.884Z" }, - { url = "https://files.pythonhosted.org/packages/ac/dc/00ae1109b76aca86d094518c9f660a53e539f7b62e643b7789cb30d5f8a4/librt-0.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a38575adf344ca7423bfb10c3a7b5df066dfbe9b95e8b35f1f79eb84e4b38cad", size = 203209, upload-time = "2025-12-06T12:04:20.306Z" }, - { url = "https://files.pythonhosted.org/packages/41/83/56161adc5890e687e11d410584479f66a32df55ec2c87a0bdc5601098a12/librt-0.7.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2dcae85482674912bdd9dc98c6a236a9698c2c13ee53366a996851e3460da26a", size = 196706, upload-time = "2025-12-06T12:04:22.027Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/37ccef8860664bebea2ea98dd0a7b1a43a678dbd5d8133f55e34fc694271/librt-0.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f02f9a7a8b720ae3c46b4df736a71d2ef07b59f3149180ad1e1eba7fccabaadf", size = 217211, upload-time = "2025-12-06T12:04:23.36Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ad/7ddc52496decd84993cb7ce7b26d02b3ce854336090f179dc90db24f0de0/librt-0.7.2-cp314-cp314t-win32.whl", hash = "sha256:062de7065ec0d060f0541602a16bed566c4b948aa1d8466c483bb949e27e0ef7", size = 45589, upload-time = "2025-12-06T12:04:25.588Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/2b5b583a089e6ed9dc5f37df3429fd408b01b982dd4bcb81dad011833bd8/librt-0.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fb6a190f76a687b034362e610c4990306ad0d913e98a8e588dcec91486797869", size = 53003, upload-time = "2025-12-06T12:04:26.831Z" }, - { url = "https://files.pythonhosted.org/packages/d9/aa/6fde1d845d01d891861f39a509a53a9390d0f4a311c032e1a2effe5b960b/librt-0.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:35e1c435ee1e24ba2b018172a3ed1caed5275168a016e560e695057acd532add", size = 45650, upload-time = "2025-12-06T12:04:28.131Z" }, +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/3e61dff6c07a3b400fe907d3164b92b3b3023ef86eac1ee236869dc276f7/librt-0.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc300cb5a5a01947b1ee8099233156fdccd5001739e5f596ecfbc0dab07b5a3b", size = 54708, upload-time = "2025-12-15T16:51:03.752Z" }, + { url = "https://files.pythonhosted.org/packages/87/98/ab2428b0a80d0fd67decaeea84a5ec920e3dd4d95ecfd074c71f51bd7315/librt-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee8d3323d921e0f6919918a97f9b5445a7dfe647270b2629ec1008aa676c0bc0", size = 56656, upload-time = "2025-12-15T16:51:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/de1fad3a16e4fb5b6605bd6cbe6d0e5207cc8eca58993835749a1da0812b/librt-0.7.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:95cb80854a355b284c55f79674f6187cc9574df4dc362524e0cce98c89ee8331", size = 161024, upload-time = "2025-12-15T16:51:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/ddfcdc1147dd7fb68321d7b064b12f0b9101d85f466a46006f86096fde8d/librt-0.7.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca1caedf8331d8ad6027f93b52d68ed8f8009f5c420c246a46fe9d3be06be0f", size = 169529, upload-time = "2025-12-15T16:51:07.907Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b3/915702c7077df2483b015030d1979404474f490fe9a071e9576f7b26fef6/librt-0.7.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a6f1236151e6fe1da289351b5b5bce49651c91554ecc7b70a947bced6fe212", size = 183270, upload-time = "2025-12-15T16:51:09.164Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/ab2f217e8ec509fca4ea9e2e5022b9f72c1a7b7195f5a5770d299df807ea/librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783", size = 179038, upload-time = "2025-12-15T16:51:10.538Z" }, + { url = "https://files.pythonhosted.org/packages/10/1c/d40851d187662cf50312ebbc0b277c7478dd78dbaaf5ee94056f1d7f2f83/librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979", size = 173502, upload-time = "2025-12-15T16:51:11.888Z" }, + { url = "https://files.pythonhosted.org/packages/07/52/d5880835c772b22c38db18660420fa6901fd9e9a433b65f0ba9b0f4da764/librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3", size = 193570, upload-time = "2025-12-15T16:51:13.168Z" }, + { url = "https://files.pythonhosted.org/packages/f1/35/22d3c424b82f86ce019c0addadf001d459dfac8036aecc07fadc5c541053/librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997", size = 42596, upload-time = "2025-12-15T16:51:14.422Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/e7c316ac5fe60ac1fdfe515198087205220803c4cf923ee63e1cb8380b17/librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8", size = 48972, upload-time = "2025-12-15T16:51:15.516Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, + { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, + { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, + { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, ] [[package]] @@ -1226,48 +1241,48 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "librt" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, - { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -1314,11 +1329,11 @@ wheels = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] @@ -1450,7 +1465,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1459,9 +1474,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] @@ -1526,11 +1541,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, ] [[package]] @@ -1557,7 +1572,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1568,9 +1583,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -1874,28 +1889,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, - { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, - { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, - { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, - { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, - { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, - { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, - { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] @@ -2126,11 +2141,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -2147,11 +2162,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.0" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] [[package]] @@ -2168,28 +2183,28 @@ wheels = [ [[package]] name = "uv" -version = "0.9.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/21/1a01209d34d49135151edd058bfefe395fd8c7f17233754d85c036311c4c/uv-0.9.16.tar.gz", hash = "sha256:b73269213e22e8638d14d0f8ae1bef34a0a3c20a3bd2010544456d36159e357d", size = 3806010, upload-time = "2025-12-06T14:19:17.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/3c/dfea2ce3f863f5fe3762d6305ff54b05d49c36d531c452e0483b226899b4/uv-0.9.16-py3-none-linux_armv6l.whl", hash = "sha256:748b6d408429d9d9ee3e59a33e714bf41471b8534c8fc1526e0d8b695c7304e1", size = 21086205, upload-time = "2025-12-06T14:18:49.667Z" }, - { url = "https://files.pythonhosted.org/packages/21/73/9b8059692dff670b10cc91aa7fb130397e35e22895f47a0d87b1fcd3c1b9/uv-0.9.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a4add59e5fb179ff01a8dc02cd24a9c7dcd8a60d3744c2dfacf2818eb709a1de", size = 20271218, upload-time = "2025-12-06T14:19:36.686Z" }, - { url = "https://files.pythonhosted.org/packages/73/d3/2f81803f4fe818b8a1f0c256523a1fed17372d8b901798a92b6316c42757/uv-0.9.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ac60c04510e4710370762c8d7f9382f269b881efacc4262e2229ef27df39441c", size = 18831407, upload-time = "2025-12-06T14:19:03.524Z" }, - { url = "https://files.pythonhosted.org/packages/16/42/13bd057513b7616ec0416d070186e410474f8f9c9fa48b965561a8d214af/uv-0.9.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5cacb026d93e9be53f9c74ee4907d2df8c3d94c7b24b1c3130f0aee62b6a0b86", size = 20551474, upload-time = "2025-12-06T14:18:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/94/83/94c46b6f00fb9602cdb1c4f38f0226643f0151ba082544c516d866af84c8/uv-0.9.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd1a625a4cd13a45a667297c30cceb1393167c413ee3fb7ed46606a857abb4fd", size = 20704842, upload-time = "2025-12-06T14:19:20.861Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0c/30fa6f16f31931d20db626f607783ada5e2f01d2307ee51fc477054b779b/uv-0.9.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69b405c7de06a8290eae6b246397ad2a3bda8b52e3b8f445a8417d9f62d938e8", size = 21681575, upload-time = "2025-12-06T14:18:54.972Z" }, - { url = "https://files.pythonhosted.org/packages/3c/e6/6a6acdde5d7df54c65ea277fded6e480ff79ffc00d6c8c3404d0142ca5d5/uv-0.9.16-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7e2ea8e2c8e77c6d3406a66155b07b445107085d240fe52e33d7f5180f356028", size = 23322212, upload-time = "2025-12-06T14:19:11.298Z" }, - { url = "https://files.pythonhosted.org/packages/1d/40/189099b44b9bd02d594dedafcd5da39f4d758e4690c504b624a61cb9eea8/uv-0.9.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f231876fa98247e8e009a91a5ea4c032e2f3315f141510259507f847f154254d", size = 22901411, upload-time = "2025-12-06T14:19:24.856Z" }, - { url = "https://files.pythonhosted.org/packages/a7/76/f09c9967648dc22c01d6cf8ce9eeb8b83fdf19c6f0a4090607be608dbcf9/uv-0.9.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c43af679a125f41b2f0fe9801ec640e9971d6c24d9e3bea2eaea4d56f240d5ed", size = 21970469, upload-time = "2025-12-06T14:19:15.531Z" }, - { url = "https://files.pythonhosted.org/packages/d1/24/3d737f69753143bba3808d18a1ec7e972cf5d337fbe1dbad6223a3d8d88f/uv-0.9.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13217422d30f5c70d37409dd5064d8dbc5a58c1cbaa29f081f43586195a50cc9", size = 22009128, upload-time = "2025-12-06T14:19:28.771Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/261d30ac548290bf13c743101c4a08bc3c37f001d3a45b8d0684fe2d151a/uv-0.9.16-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f06a6e172f34c70865784ff9108a1eabc5a99c97363d9ee587884b111bb220d2", size = 20698915, upload-time = "2025-12-06T14:19:40.497Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/be1651da4398ee7d5064e712a80f0ad83dc47533531f78fb35ae237f1917/uv-0.9.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:307336087b53e0a1e6e1c7ec4633ca0bf11be786c692e23c3a343cac37b3e013", size = 21936423, upload-time = "2025-12-06T14:18:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/81/2d/953ddab1cbef688ceb365b571249ce333e7bc8a70af894d1f85e969dc427/uv-0.9.16-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0b1e32a5c4024b8628b2799d407ffa7aa913ca1554258e963a516936119baba1", size = 20656496, upload-time = "2025-12-06T14:19:44.2Z" }, - { url = "https://files.pythonhosted.org/packages/e8/9d/ef3ff37c6485a3d482c774da0cdb0cb7415a3e267af3b000978c10e0264b/uv-0.9.16-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c21aa40106a902a531a3890d414dd3a418db4a17275f3a3829d08ddc1888bb2d", size = 21154195, upload-time = "2025-12-06T14:19:07.217Z" }, - { url = "https://files.pythonhosted.org/packages/b5/8b/645a28fa9ff93dfe037385a04e27b45cd7335173c7c6239d157b6a09d623/uv-0.9.16-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0d012ef231699cad1eaf9ab1f5b076522970dcf87bfba3a684e66a02c6a1575d", size = 22207319, upload-time = "2025-12-06T14:18:59.521Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/c4e8bc5b759d5da4fcabe0b1fc876f67913da4880d0cec34e9e6418ff93c/uv-0.9.16-py3-none-win32.whl", hash = "sha256:0229a4dfd0ff7e257bcd791f2d78cf1d682b01856d52d95602c56bb5ed97cc72", size = 19908839, upload-time = "2025-12-06T14:18:46.032Z" }, - { url = "https://files.pythonhosted.org/packages/3b/21/6ecf7db074235552d7b0be84c48d934e9916809d7dafb96d9a1019dd2ded/uv-0.9.16-py3-none-win_amd64.whl", hash = "sha256:e3e9a69a463607b9886afa34ce68dadf9a378eb6d191c878156fd8864e604c1e", size = 22033348, upload-time = "2025-12-06T14:19:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/db/a1/4c44988817b72b17f09010983fd40b05f76ce54988fbdd707a8a33cfd498/uv-0.9.16-py3-none-win_arm64.whl", hash = "sha256:18d430980e7f4915a42854bc98a76f87f30da8859469a864fcf33e0a31fafdd1", size = 20396419, upload-time = "2025-12-06T14:19:32.586Z" }, +version = "0.9.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/03/1afff9e6362dc9d3a9e03743da0a4b4c7a0809f859c79eb52bbae31ea582/uv-0.9.18.tar.gz", hash = "sha256:17b5502f7689c4dc1fdeee9d8437a9a6664dcaa8476e70046b5f4753559533f5", size = 3824466, upload-time = "2025-12-16T15:45:11.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9c/92fad10fcee8ea170b66442d95fd2af308fe9a107909ded4b3cc384fdc69/uv-0.9.18-py3-none-linux_armv6l.whl", hash = "sha256:e9e4915bb280c1f79b9a1c16021e79f61ed7c6382856ceaa99d53258cb0b4951", size = 21345538, upload-time = "2025-12-16T15:45:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/81/b1/b0e5808e05acb54aa118c625d9f7b117df614703b0cbb89d419d03d117f3/uv-0.9.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d91abfd2649987996e3778729140c305ef0f6ff5909f55aac35c3c372544a24f", size = 20439572, upload-time = "2025-12-16T15:45:26.397Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0b/9487d83adf5b7fd1e20ced33f78adf84cb18239c3d7e91f224cedba46c08/uv-0.9.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cf33f4146fd97e94cdebe6afc5122208eea8c55b65ca4127f5a5643c9717c8b8", size = 18952907, upload-time = "2025-12-16T15:44:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/58/92/c8f7ae8900eff8e4ce1f7826d2e1e2ad5a95a5f141abdb539865aff79930/uv-0.9.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:edf965e9a5c55f74020ac82285eb0dfe7fac4f325ad0a7afc816290269ecfec1", size = 20772495, upload-time = "2025-12-16T15:45:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/5a/28/9831500317c1dd6cde5099e3eb3b22b88ac75e47df7b502f6aef4df5750e/uv-0.9.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae10a941bd7ca1ee69edbe3998c34dce0a9fc2d2406d98198343daf7d2078493", size = 20949623, upload-time = "2025-12-16T15:44:57.482Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ff/1fe1ffa69c8910e54dd11f01fb0765d4fd537ceaeb0c05fa584b6b635b82/uv-0.9.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1669a95b588f613b13dd10e08ced6d5bcd79169bba29a2240eee87532648790", size = 21920580, upload-time = "2025-12-16T15:44:39.009Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/eed3ec7679ee80e16316cfc95ed28ef6851700bcc66edacfc583cbd2cc47/uv-0.9.18-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:11e1e406590d3159138288203a41ff8a8904600b8628a57462f04ff87d62c477", size = 23491234, upload-time = "2025-12-16T15:45:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/78/58/64b15df743c79ad03ea7fbcbd27b146ba16a116c57f557425dd4e44d6684/uv-0.9.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e82078d3c622cb4c60da87f156168ffa78b9911136db7ffeb8e5b0a040bf30e", size = 23095438, upload-time = "2025-12-16T15:45:17.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/6d/3d3dae71796961603c3871699e10d6b9de2e65a3c327b58d4750610a5f93/uv-0.9.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704abaf6e76b4d293fc1f24bef2c289021f1df0de9ed351f476cbbf67a7edae0", size = 22140992, upload-time = "2025-12-16T15:44:45.527Z" }, + { url = "https://files.pythonhosted.org/packages/31/91/1042d0966a30e937df500daed63e1f61018714406ce4023c8a6e6d2dcf7c/uv-0.9.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3332188fd8d96a68e5001409a52156dced910bf1bc41ec3066534cffcd46eb68", size = 22229626, upload-time = "2025-12-16T15:45:20.712Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1f/0a4a979bb2bf6e1292cc57882955bf1d7757cad40b1862d524c59c2a2ad8/uv-0.9.18-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:b7295e6d505f1fd61c54b1219e3b18e11907396333a9fa61cefe489c08fc7995", size = 20896524, upload-time = "2025-12-16T15:45:06.799Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3c/24f92e56af00cac7d9bed2888d99a580f8093c8745395ccf6213bfccf20b/uv-0.9.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:62ea0e518dd4ab76e6f06c0f43a25898a6342a3ecf996c12f27f08eb801ef7f1", size = 22077340, upload-time = "2025-12-16T15:44:51.271Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3e/73163116f748800e676bf30cee838448e74ac4cc2f716c750e1705bc3fe4/uv-0.9.18-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8bd073e30030211ba01206caa57b4d63714e1adee2c76a1678987dd52f72d44d", size = 20932956, upload-time = "2025-12-16T15:45:00.3Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/a26990b51a17de1ffe41fbf2e30de3a98f0e0bce40cc60829fb9d9ed1a8a/uv-0.9.18-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f248e013d10e1fc7a41f94310628b4a8130886b6d683c7c85c42b5b36d1bcd02", size = 21357247, upload-time = "2025-12-16T15:45:23.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/20/b6ba14fdd671e9237b22060d7422aba4a34503e3e42d914dbf925eff19aa/uv-0.9.18-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:17bedf2b0791e87d889e1c7f125bd5de77e4b7579aec372fa06ba832e07c957e", size = 22443585, upload-time = "2025-12-16T15:44:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/1b3dd596964f90a122cfe94dcf5b6b89cf5670eb84434b8c23864382576f/uv-0.9.18-py3-none-win32.whl", hash = "sha256:de6f0bb3e9c18e484545bd1549ec3c956968a141a393d42e2efb25281cb62787", size = 20091088, upload-time = "2025-12-16T15:45:03.225Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/50e13ebc1eedb36d88524b7740f78351be33213073e3faf81ac8925d0c6e/uv-0.9.18-py3-none-win_amd64.whl", hash = "sha256:c82b0e2e36b33e2146fba5f0ae6906b9679b3b5fe6a712e5d624e45e441e58e9", size = 22181193, upload-time = "2025-12-16T15:44:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/0bf338d863a3d9e5545e268d77a8e6afdd75d26bffc939603042f2e739f9/uv-0.9.18-py3-none-win_arm64.whl", hash = "sha256:4c4ce0ed080440bbda2377488575d426867f94f5922323af6d4728a1cd4d091d", size = 20564933, upload-time = "2025-12-16T15:45:09.819Z" }, ] [[package]] From 5f27073e552b5de7754122b5aa3d3078938401db Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 15:55:08 -0500 Subject: [PATCH 07/33] fix partial date test --- README.md | 3 +++ tests/test_cli.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17f0265..9cdeae1 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,9 @@ See [CHANGELOG.md](https://github.com/viseshrp/workedon/blob/main/CHANGELOG.md) - The [date parser](https://github.com/scrapinghub/dateparser) which is used may misinterpret some irregular phrases of date/time, but mostly does great. +- For partial dates (e.g., "August 1947"), dateparser fills missing parts + using your current date, which can change results over time based on the date. + Prefer explicit dates or ranges (e.g., "Aug 1 1947" or "--from/--to or --on"). - `#` and `[]` are reserved characters in the description. - `#` is used to specify tags. You can use it multiple times to specify diff --git a/tests/test_cli.py b/tests/test_cli.py index 7546c61..976d078 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -185,7 +185,7 @@ def test_timezone_option( ("learning guitar @ 9pm friday", ["--at", "9pm friday"]), # 21 ( "gaining Indian Independence @ 1pm August 15 1947", - ["--since", "August 1947", "-r", "-n", "1"], + ["--since", "August 1 1947", "-r", "-n", "1"], ), # 22 ("framing a photo @ 1:34pm yesterday", ["--yesterday"]), # 23 ("taking pictures @ 12:34pm yesterday", ["-e"]), # 24 From d4144918996b9672a78c7894dbdbc7537252e00d Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 16:12:46 -0500 Subject: [PATCH 08/33] add prefetch for tags --- workedon/models.py | 6 +++++- workedon/workedon.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/workedon/models.py b/workedon/models.py index 564e168..63090b3 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -82,7 +82,11 @@ def __str__(self) -> str: timestamp_str = user_time.strftime( settings.DATETIME_FORMAT or f"{settings.DATE_FORMAT} {settings.TIME_FORMAT}" ) - tags = [t.tag.name for t in self.tags.order_by(WorkTag.tag.name)] + tags_rel = self.tags + if hasattr(tags_rel, "order_by"): + tags = [t.tag.name for t in tags_rel.order_by(WorkTag.tag.name)] + else: + tags = sorted([t.tag.name for t in tags_rel]) tags_str = f"Tags: {', '.join(tags)}\n" if tags else "" if self.duration is not None: diff --git a/workedon/workedon.py b/workedon/workedon.py index fcd9395..42c038d 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -9,7 +9,7 @@ from typing import Any import click -from peewee import ModelSelect, chunked +from peewee import ModelSelect, chunked, prefetch from .constants import WORK_CHUNK_SIZE from .exceptions import ( @@ -66,6 +66,29 @@ def _generate_work(result: Iterator[Work]) -> Iterator[str]: yield str(work) +def chunked_prefetch_generator( + work_set: ModelSelect, + fields: list[Any], + text_only: bool, +) -> Iterator[str]: + """ + Fetch work in chunks and prefetch tags to avoid N+1 queries. + """ + for chunk in chunked(work_set.iterator(), WORK_CHUNK_SIZE): + if text_only: + for work in chunk: + yield str(work) + else: + chunk_uuids = [work.uuid for work in chunk] + chunk_query = Work.select(*fields).where(Work.uuid.in_(chunk_uuids)) + chunk_with_tags = prefetch(chunk_query, WorkTag, Tag) + work_dict = {work.uuid: work for work in chunk_with_tags} + for uuid in chunk_uuids: + work = work_dict.get(uuid) + if work is not None: + yield str(work) + + def _get_date_range( start_date: str, end_date: str, @@ -200,11 +223,16 @@ def fetch_work( return if work_count == 1 or no_page: - for work in work_set: + # Prefetch tags when rendering full work entries. + work_result = work_set if text_only else prefetch(work_set, WorkTag, Tag) + for work in work_result: click.echo(work, nl=False) else: - gen = work_set.iterator() - click.echo_via_pager(_generate_work(gen)) + # Large result set with pagination - use chunked prefetching + # This loads results in chunks, prefetching tags for each chunk + # to avoid N+1 queries while keeping memory usage bounded + gen = chunked_prefetch_generator(work_set, fields, text_only) + click.echo_via_pager(gen) except Exception as e: raise CannotFetchWorkError(extra_detail=str(e)) from e From 527064ed787dbf76676c12b4ccac921d55fe44c2 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 16:16:19 -0500 Subject: [PATCH 09/33] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cdeae1..9b857eb 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ See [CHANGELOG.md](https://github.com/viseshrp/workedon/blob/main/CHANGELOG.md) used may misinterpret some irregular phrases of date/time, but mostly does great. - For partial dates (e.g., "August 1947"), dateparser fills missing parts - using your current date, which can change results over time based on the date. + using your current date, which can change results over time based on the date. Prefer explicit dates or ranges (e.g., "Aug 1 1947" or "--from/--to or --on"). - `#` and `[]` are reserved characters in the description. From 12e716b3d1b8f9e66d7daa6fe78026fe51cc55d3 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 16:45:24 -0500 Subject: [PATCH 10/33] Update workedon.py --- workedon/workedon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workedon/workedon.py b/workedon/workedon.py index 42c038d..ce7cb90 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -79,6 +79,8 @@ def chunked_prefetch_generator( for work in chunk: yield str(work) else: + # This re-queries the chunk to attach tags, plus prefetch issues + # additional queries for WorkTag and Tag to avoid N+1 lookups. chunk_uuids = [work.uuid for work in chunk] chunk_query = Work.select(*fields).where(Work.uuid.in_(chunk_uuids)) chunk_with_tags = prefetch(chunk_query, WorkTag, Tag) From 76ce41f2f971068992b9bdcfa90ea6fee87af583 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 17:57:39 -0500 Subject: [PATCH 11/33] improve tag filter queries --- workedon/workedon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/workedon/workedon.py b/workedon/workedon.py index ce7cb90..2095f50 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -176,7 +176,9 @@ def fetch_work( # tag if tags: normalized = [t.lower() for t in tags] - work_set = work_set.join(WorkTag).join(Tag).where(Tag.name.in_(normalized)).distinct() + tag_ids = Tag.select(Tag.uuid).where(Tag.name.in_(normalized)) + work_ids = WorkTag.select(WorkTag.work).where(WorkTag.tag.in_(tag_ids)) + work_set = work_set.where(Work.uuid.in_(work_ids)) # duration if duration: # Match optional comparison operator and value (e.g., '>=3h', '<= 45min', '2h') From 4d4a26a1b1f56dcef709b7c5c2ba35676bdfcdc8 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 18:42:01 -0500 Subject: [PATCH 12/33] delay COUNT query until later --- workedon/workedon.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/workedon/workedon.py b/workedon/workedon.py index 2095f50..63f6e75 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -212,21 +212,22 @@ def fetch_work( # fetch from db now. try: with init_db(): - work_count = work_set.count() + has_work = work_set.exists() if delete: - if work_count > 0 and click.confirm(f"Continue deleting {work_count} log(s)?"): - click.echo("Deleting...") - deleted = Work.delete().where(Work.uuid.in_(work_set)).execute() - click.echo(f"{deleted} log(s) deleted successfully.") - elif work_count == 0: + if has_work: + if click.confirm(f"Continue deleting log(s)?"): + click.echo("Deleting...") + deleted_count = Work.delete().where(Work.uuid.in_(work_set)).execute() + click.echo(f"{deleted_count} log(s) deleted successfully.") + else: click.echo("Nothing to delete.") return - if work_count == 0: + if not has_work: click.echo("Nothing to show, slacker.") return - if work_count == 1 or no_page: + if no_page or work_set.count() == 1: # Prefetch tags when rendering full work entries. work_result = work_set if text_only else prefetch(work_set, WorkTag, Tag) for work in work_result: From fbf7086ed61c70da1635387b5b26e8f96eca7015 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 19:01:11 -0500 Subject: [PATCH 13/33] Update workedon.py --- workedon/workedon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workedon/workedon.py b/workedon/workedon.py index 63f6e75..f39165d 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -215,7 +215,7 @@ def fetch_work( has_work = work_set.exists() if delete: if has_work: - if click.confirm(f"Continue deleting log(s)?"): + if click.confirm("Continue deleting log(s)?"): click.echo("Deleting...") deleted_count = Work.delete().where(Work.uuid.in_(work_set)).execute() click.echo(f"{deleted_count} log(s) deleted successfully.") From 2bbbe5395ddaf1f3f380d68c9e3300851be8b744 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 22:49:52 -0500 Subject: [PATCH 14/33] add tests by Codex and Claude --- tests/test_cli.py | 57 ++++- tests/test_cli_branches.py | 34 +++ tests/test_conf.py | 95 ++++++++ tests/test_constants.py | 8 + tests/test_default_settings.py | 10 + tests/test_exceptions.py | 32 +++ tests/test_integration.py | 111 +++++++++ tests/test_models.py | 213 ++++++++++++++++++ tests/test_models_edge_cases.py | 119 ++++++++++ tests/test_parser.py | 70 ++++++ tests/test_parser_edge_cases.py | 377 +++++++++++++++++++++++++++++++ tests/test_utils.py | 96 ++++++++ tests/test_utils_edge_cases.py | 70 ++++++ tests/test_version_main.py | 35 +++ tests/test_workedon.py | 388 ++++++++++++++++++++++++++++++++ 15 files changed, 1710 insertions(+), 5 deletions(-) create mode 100644 tests/test_cli_branches.py create mode 100644 tests/test_conf.py create mode 100644 tests/test_constants.py create mode 100644 tests/test_default_settings.py create mode 100644 tests/test_exceptions.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_models.py create mode 100644 tests/test_models_edge_cases.py create mode 100644 tests/test_parser.py create mode 100644 tests/test_parser_edge_cases.py create mode 100644 tests/test_utils.py create mode 100644 tests/test_utils_edge_cases.py create mode 100644 tests/test_version_main.py create mode 100644 tests/test_workedon.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 976d078..4a4c43e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -58,6 +58,11 @@ def test_empty_fetch(runner: CliRunner) -> None: assert "Nothing to show" in result.output +def test_main_with_subcommand_returns_early(runner: CliRunner) -> None: + result = runner.invoke(cli.main, ["what", "--no-page"]) + assert result.exit_code == 0 + + # -- Basic save & fetch scenarios ------------------------------------------------ @@ -100,6 +105,15 @@ def test_fetch_last(runner: CliRunner, command: str, description: str, valid: bo assert description not in result.output +def test_fetch_last_returns_most_recent_entry(runner: CliRunner) -> None: + save_and_verify(runner, "first thing @ 3 days ago", "first thing") + save_and_verify(runner, "second thing @ yesterday", "second thing") + + result = runner.invoke(cli.what, ["--no-page", "--last"]) + verify_work_output(result, "second thing") + assert "first thing" not in result.output + + # -- Fetch by ID ---------------------------------------------------------------- @@ -220,6 +234,15 @@ def test_save_and_fetch_others(runner: CliRunner, command: str, flag: list[str]) verify_work_output(result, description) +def test_default_fetch_excludes_entries_older_than_week(runner: CliRunner) -> None: + save_and_verify(runner, "ancient history @ 10 days ago", "ancient history") + save_and_verify(runner, "fresh work @ yesterday", "fresh work") + + result = runner.invoke(cli.what, ["--no-page"]) + verify_work_output(result, "fresh work") + assert "ancient history" not in result.output + + # -- Deletion -------------------------------------------------------------------- @@ -477,6 +500,16 @@ def test_cli_tag_filter( assert "Nothing to show" in result_fetch.output +def test_list_tags_outputs_saved_tags(runner: CliRunner) -> None: + runner.invoke(cli.main, ["first", "tag", "#alpha"]) + runner.invoke(cli.main, ["second", "tag", "#beta"]) + + result = runner.invoke(cli.main, ["--list-tags"]) + assert result.exit_code == 0 + assert "alpha" in result.output.lower() + assert "beta" in result.output.lower() + + # -- Duration ------------------------------------------------------------ @@ -494,13 +527,10 @@ def test_cli_tag_filter( def test_cli_duration_parsing_and_display( runner: CliRunner, input_text: str, expected_duration: str, xargs: list ) -> None: - result_save = runner.invoke(cli.main, input_text.split()) + result_save = runner.invoke(cli.main, [*input_text.split(), *xargs]) assert result_save.exit_code == 0 assert "Work saved." in result_save.output - - result_fetch = runner.invoke(cli.what, ["--no-page", "--last", *xargs]) - assert result_fetch.exit_code == 0 - assert expected_duration in result_fetch.output + assert expected_duration in result_save.output @pytest.mark.parametrize( @@ -561,3 +591,20 @@ def test_cli_duration_filter( def test_invalid_duration_filter(runner: CliRunner, invalid_filter_flag: list[str]) -> None: result = runner.invoke(cli.what, ["--no-page", *invalid_filter_flag]) assert result.exit_code == 1 + assert "Invalid duration" in result.output + + +def test_cli_duration_option_overrides_inline_value(runner: CliRunner) -> None: + save_result = runner.invoke(cli.main, ["overridden task", "[30m]", "--duration", "2h"]) + verify_work_output(save_result, "overridden task") + + fetch_result = runner.invoke(cli.what, ["--no-page", "--last"]) + assert "Duration: 120.0 minutes" in fetch_result.output + + +def test_cli_duration_option_ignored_when_invalid(runner: CliRunner) -> None: + save_result = runner.invoke(cli.main, ["keep inline", "[30m]", "--duration", "abc"]) + verify_work_output(save_result, "keep inline") + + fetch_result = runner.invoke(cli.what, ["--no-page", "--last"]) + assert "Duration: 30.0 minutes" in fetch_result.output diff --git a/tests/test_cli_branches.py b/tests/test_cli_branches.py new file mode 100644 index 0000000..aad5451 --- /dev/null +++ b/tests/test_cli_branches.py @@ -0,0 +1,34 @@ +import importlib +import os + +import pytest + +from workedon import cli +import workedon + + +def test_cli_import_skips_warning_filter_in_debug(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("WORKEDON_DEBUG", "1") + importlib.reload(cli) + importlib.reload(workedon) + assert cli.CONTEXT_SETTINGS["help_option_names"] == ["-h", "--help"] + monkeypatch.delenv("WORKEDON_DEBUG", raising=False) + importlib.reload(cli) + importlib.reload(workedon) + + +def test_main_callback_runs_without_list_tags(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(cli.settings, "configure", lambda *args, **kwargs: None) + with cli.main.make_context("workedon", []) as ctx: + ctx.invoked_subcommand = None + with ctx: + cli.main.callback( + settings_path=False, + print_settings=False, + list_tags=False, + db_version=False, + sqlite_version=False, + print_db_path=False, + vacuum_db=False, + truncate_db=False, + ) diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..362d130 --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,95 @@ +from pathlib import Path + +import pytest + +from workedon import conf +from workedon.conf import Settings +from workedon.constants import SETTINGS_HEADER +from workedon.exceptions import CannotCreateSettingsError, CannotLoadSettingsError + + +def test_settings_getattr_and_setattr() -> None: + settings = Settings() + settings.FOO = "bar" + assert settings.FOO == "bar" + assert settings["FOO"] == "bar" + + +def test_configure_creates_settings_file( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + + settings = Settings() + settings.configure() + + assert conf_path.exists() + assert SETTINGS_HEADER.strip() in conf_path.read_text() + + +def test_configure_loads_user_settings( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + conf_path.write_text('TIME_FORMAT = "%H:%M"\n') + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + + settings = Settings() + settings.configure() + + assert settings.TIME_FORMAT == "%H:%M" + + +def test_configure_merges_user_settings( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + conf_path.write_text('DATE_FORMAT = "%Y"\n') + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + + settings = Settings() + settings.configure(user_settings={"DATE_FORMAT": "%d"}) + + assert settings.DATE_FORMAT == "%d" + + +def test_configure_raises_on_bad_spec( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + conf_path.write_text("# ok\n") + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + monkeypatch.setattr(conf, "spec_from_file_location", lambda *args, **kwargs: None) + + settings = Settings() + with pytest.raises(CannotLoadSettingsError): + settings.configure() + + +def test_configure_raises_on_exec_module_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + conf_path.write_text("raise RuntimeError('boom')\n") + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + + settings = Settings() + with pytest.raises(CannotLoadSettingsError): + settings.configure() + + +def test_configure_raises_on_settings_file_creation_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + conf_path = tmp_path / "wonfile.py" + monkeypatch.setattr(conf, "CONF_PATH", conf_path) + + settings = Settings() + + def blow_up() -> None: + raise OSError("nope") + + monkeypatch.setattr(Settings, "_create_settings_file", blow_up) + with pytest.raises(CannotCreateSettingsError): + settings.configure() diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..a4e5df2 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,8 @@ +from workedon import constants + + +def test_constants_have_expected_values() -> None: + assert constants.APP_NAME == "workedon" + assert constants.CURRENT_DB_VERSION > 0 + assert constants.WORK_CHUNK_SIZE > 0 + assert "workedon settings file" in constants.SETTINGS_HEADER diff --git a/tests/test_default_settings.py b/tests/test_default_settings.py new file mode 100644 index 0000000..de58a1e --- /dev/null +++ b/tests/test_default_settings.py @@ -0,0 +1,10 @@ +from workedon import default_settings + + +def test_default_settings_are_sane() -> None: + assert isinstance(default_settings.DATE_FORMAT, str) + assert isinstance(default_settings.TIME_FORMAT, str) + assert default_settings.DATETIME_FORMAT == "" + assert isinstance(default_settings.TIME_ZONE, str) + assert default_settings.TIME_ZONE + assert default_settings.DURATION_UNIT == "minutes" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..c24a77f --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,32 @@ +import pytest + +from workedon import exceptions + + +@pytest.mark.parametrize( + "exc_cls, detail", + [ + (exceptions.DBInitializationError, "Unable to initialize the database."), + (exceptions.CannotCreateSettingsError, "Unable to create settings file."), + (exceptions.CannotLoadSettingsError, "Unable to load settings file."), + (exceptions.InvalidWorkError, "The provided work text is invalid."), + ( + exceptions.InvalidDateTimeError, + "The provided date/time is invalid. Please refer the docs for valid phrases.", + ), + (exceptions.DateTimeInFutureError, "The provided date/time is in the future."), + (exceptions.StartDateAbsentError, "Please provide a start date/time."), + ( + exceptions.StartDateGreaterError, + "The provided start date/time is greater than the end date/time.", + ), + (exceptions.CannotSaveWorkError, "Unable to save your work."), + (exceptions.CannotFetchWorkError, "Unable to fetch your work."), + ], +) +def test_exception_details_and_string_formatting( + exc_cls: type[exceptions.WorkedOnError], detail: str +) -> None: + assert str(exc_cls()) == detail + assert str(exc_cls(extra_detail="extra")) == f"{detail} :: extra" + assert str(exc_cls(extra_detail="")) == detail diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..c4e6246 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,111 @@ +"""Integration tests covering complete workflows.""" + +from click.testing import CliRunner +import pytest + +from workedon import cli + + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +def test_full_workflow_save_modify_fetch_delete(runner: CliRunner) -> None: + # Save work + save_result = runner.invoke(cli.main, ["working on feature #dev [2h] @ yesterday"]) + assert save_result.exit_code == 0 + assert "Work saved." in save_result.output + + # Fetch by tag + fetch_result = runner.invoke(cli.what, ["--no-page", "--tag", "dev"]) + assert fetch_result.exit_code == 0 + assert "working on feature" in fetch_result.output + + # Fetch by duration + duration_result = runner.invoke(cli.what, ["--no-page", "--duration", "=2h"]) + assert duration_result.exit_code == 0 + assert "working on feature" in duration_result.output + + # Delete + delete_result = runner.invoke( + cli.what, ["--no-page", "--tag", "dev", "--delete"], input="y" + ) + assert delete_result.exit_code == 0 + assert "deleted successfully" in delete_result.output + + # Verify deletion + verify_result = runner.invoke(cli.what, ["--no-page", "--tag", "dev"]) + assert verify_result.exit_code == 0 + assert "Nothing to show" in verify_result.output + + +def test_multiple_tags_filtering(runner: CliRunner) -> None: + # Save work with multiple tags + runner.invoke(cli.main, ["task1 #dev #frontend @ 1pm yesterday"]) + runner.invoke(cli.main, ["task2 #dev #backend @ 2pm yesterday"]) + runner.invoke(cli.main, ["task3 #qa #frontend @ 3pm yesterday"]) + + # Filter by single tag + dev_result = runner.invoke(cli.what, ["--no-page", "--tag", "dev", "--yesterday"]) + assert "task1" in dev_result.output + assert "task2" in dev_result.output + assert "task3" not in dev_result.output + + # Filter by multiple tags (AND logic) + multi_result = runner.invoke( + cli.what, ["--no-page", "--tag", "dev", "--tag", "frontend", "--yesterday"] + ) + assert "task1" in multi_result.output + assert "task2" in multi_result.output + assert "task3" in multi_result.output + + +def test_duration_with_timezone_changes(runner: CliRunner) -> None: + # Save in one timezone + runner.invoke(cli.main, ["work [90m] @ 3pm yesterday", "--time-zone", "UTC"]) + + # Fetch in different timezone + result = runner.invoke( + cli.what, ["--no-page", "--last", "--time-zone", "Asia/Tokyo"] + ) + assert result.exit_code == 0 + assert "Duration:" in result.output + + +def test_pagination_with_large_dataset(runner: CliRunner) -> None: + # Create many entries + for i in range(50): + runner.invoke(cli.main, [f"work item {i} @ {i} hours ago"]) + + # Fetch without pagination + no_page = runner.invoke(cli.what, ["--no-page", "--count", "50"]) + assert no_page.exit_code == 0 + + # Fetch with pagination (default behavior, harder to test) + with_page = runner.invoke(cli.what, ["--count", "50"]) + assert with_page.exit_code == 0 + + +def test_edge_case_midnight_boundary(runner: CliRunner) -> None: + # Save at midnight + runner.invoke(cli.main, ["midnight task @ 12:00am"]) + + # Should appear in today's results + today_result = runner.invoke(cli.what, ["--no-page", "--today"]) + assert "midnight task" in today_result.output + + +def test_complex_datetime_parsing(runner: CliRunner) -> None: + test_cases = [ + "meeting @ 3pm last friday", + "call @ 9:30am yesterday", + "email @ noon 3 days ago", + "standup @ 10am this week", + ] + + for case in test_cases: + result = runner.invoke(cli.main, case.split()) + assert result.exit_code == 0 or any( + keyword in result.output.lower() for keyword in ("future", "invalid") + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..815927a --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,213 @@ +from pathlib import Path + +from peewee import OperationalError, SqliteDatabase +import pytest + +from workedon import models +from workedon.conf import settings +from workedon.constants import CURRENT_DB_VERSION +from workedon.exceptions import DBInitializationError +from workedon.models import Tag, Work, WorkTag, init_db + + +@pytest.fixture(autouse=True) +def configure_settings(monkeypatch: pytest.MonkeyPatch) -> None: + settings.configure() + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + monkeypatch.setattr(settings, "internal_tz", "UTC") + + +def test_get_or_create_db_creates_file( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + db_path = tmp_path / "won.db" + monkeypatch.setattr(models, "DB_PATH", db_path) + + db = models._get_or_create_db() + try: + assert db_path.exists() + assert db.database == str(db_path) + finally: + db.close() + + +def test_get_or_create_db_uses_existing_file( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + db_path = tmp_path / "won.db" + db_path.parent.mkdir(parents=True, exist_ok=True) + db_path.write_text("") + monkeypatch.setattr(models, "DB_PATH", db_path) + + db = models._get_or_create_db() + try: + assert db_path.exists() + assert db.database == str(db_path) + finally: + db.close() + + +def test_get_and_set_db_user_version(tmp_path: Path) -> None: + db = SqliteDatabase(str(tmp_path / "version.db")) + db.connect() + try: + models._set_db_user_version(db, 5) + assert models.get_db_user_version(db) == 5 + finally: + db.close() + + +def test_apply_pending_migrations_from_zero(tmp_path: Path) -> None: + db = SqliteDatabase(str(tmp_path / "fresh.db")) + db.connect() + try: + with db.bind_ctx([Work, Tag, WorkTag]): + models._apply_pending_migrations(db) + assert models.get_db_user_version(db) == CURRENT_DB_VERSION + assert {"work", "tag", "work_tag"}.issubset(set(db.get_tables())) + finally: + db.close() + + +def test_apply_pending_migrations_from_v1( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + db = SqliteDatabase(str(tmp_path / "v1-pending.db")) + db.connect() + try: + db.execute_sql( + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME);" + ) + models._set_db_user_version(db, 1) + + def fake_migrate_v2_to_v3(database: SqliteDatabase) -> None: + models._set_db_user_version(database, CURRENT_DB_VERSION) + + monkeypatch.setattr(models, "_migrate_v2_to_v3", fake_migrate_v2_to_v3) + with db.bind_ctx([Work, Tag, WorkTag]): + models._apply_pending_migrations(db) + + assert models.get_db_user_version(db) == CURRENT_DB_VERSION + cols = [row[1] for row in db.execute_sql("PRAGMA table_info(work);").fetchall()] + assert "duration" in cols + assert {"tag", "work_tag"}.issubset(set(db.get_tables())) + finally: + db.close() + + +def test_apply_pending_migrations_from_v2(tmp_path: Path) -> None: + db = SqliteDatabase(str(tmp_path / "v2-pending.db")) + db.connect() + try: + db.execute_sql( + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" + ) + db.execute_sql( + "CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);" + ) + db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") + models._set_db_user_version(db, 2) + with db.bind_ctx([Work, Tag, WorkTag]): + models._apply_pending_migrations(db) + + assert models.get_db_user_version(db) == CURRENT_DB_VERSION + indexes = [row[1] for row in db.execute_sql("PRAGMA index_list(work);").fetchall()] + assert any("duration" in name for name in indexes) + finally: + db.close() + + +def test_migrate_v1_to_v2_adds_tables_and_duration(tmp_path: Path) -> None: + db = SqliteDatabase(str(tmp_path / "v1.db")) + db.connect() + try: + db.execute_sql( + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME);" + ) + models._set_db_user_version(db, 1) + with db.bind_ctx([Work, Tag, WorkTag]): + models._migrate_v1_to_v2(db) + + assert models.get_db_user_version(db) == 2 + assert {"tag", "work_tag"}.issubset(set(db.get_tables())) + cols = [row[1] for row in db.execute_sql("PRAGMA table_info(work);").fetchall()] + assert "duration" in cols + finally: + db.close() + + +def test_migrate_v2_to_v3_adds_duration_index(tmp_path: Path) -> None: + db = SqliteDatabase(str(tmp_path / "v2.db")) + db.connect() + try: + db.execute_sql( + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" + ) + db.execute_sql( + "CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);" + ) + db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") + models._set_db_user_version(db, 2) + with db.bind_ctx([Work, Tag, WorkTag]): + models._migrate_v2_to_v3(db) + + assert models.get_db_user_version(db) == CURRENT_DB_VERSION + indexes = [row[1] for row in db.execute_sql("PRAGMA index_list(work);").fetchall()] + assert any("duration" in name for name in indexes) + finally: + db.close() + + +def test_apply_pending_migrations_raises_on_mismatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + db = SqliteDatabase(str(tmp_path / "bad.db")) + db.connect() + try: + with db.bind_ctx([Work, Tag, WorkTag]): + monkeypatch.setattr( + models, "get_db_user_version", lambda *_: CURRENT_DB_VERSION + 1 + ) + with pytest.raises(DBInitializationError): + models._apply_pending_migrations(db) + finally: + db.close() + + +def test_apply_pending_migrations_wraps_operational_error( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + db = SqliteDatabase(str(tmp_path / "bad-op.db")) + db.connect() + try: + with db.bind_ctx([Work, Tag, WorkTag]): + def raise_operational_error(*_args, **_kwargs): + raise OperationalError("fail") + + monkeypatch.setattr(db, "execute_sql", raise_operational_error) + with pytest.raises(DBInitializationError) as excinfo: + models._apply_pending_migrations(db) + assert "fail" in str(excinfo.value) + finally: + db.close() + + +def test_truncate_all_tables_clears_rows() -> None: + with init_db(): + work = Work.create(work="cleanup test") + tag = Tag.create(name="cleanup") + WorkTag.create(work=work.uuid, tag=tag.uuid) + + models.truncate_all_tables() + + assert Work.select().count() == 0 + assert Tag.select().count() == 0 + assert WorkTag.select().count() == 0 + + +def test_tag_str_and_work_text_only_str() -> None: + tag = Tag(name="alpha") + assert "* alpha" in str(tag) + + work = Work(uuid=None, work="text only") + assert "* text only" in str(work) diff --git a/tests/test_models_edge_cases.py b/tests/test_models_edge_cases.py new file mode 100644 index 0000000..0b2e90d --- /dev/null +++ b/tests/test_models_edge_cases.py @@ -0,0 +1,119 @@ +"""Edge case tests for database models.""" + +import pytest +from peewee import IntegrityError + +from workedon.models import Tag, Work, WorkTag, init_db + + +def test_work_requires_uuid() -> None: + with init_db(): + with pytest.raises(IntegrityError): + Work.create(uuid=None, work="test") + + +def test_work_requires_work_text() -> None: + with init_db(): + with pytest.raises(IntegrityError): + Work.create(work=None) + + +def test_work_allows_null_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=None) + assert work.duration is None + + +def test_work_allows_zero_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=0) + assert work.duration == 0 + + +def test_work_allows_large_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=999999.99) + assert work.duration == 999999.99 + + +def test_work_string_representation_with_no_tags() -> None: + with init_db(): + work = Work.create(work="simple work") + output = str(work) + assert "simple work" in output + assert "Tags:" not in output + + +def test_work_string_representation_with_no_duration() -> None: + with init_db(): + work = Work.create(work="simple work", duration=None) + output = str(work) + assert "Duration:" not in output + + +def test_tag_requires_unique_name() -> None: + with init_db(): + Tag.create(name="unique") + with pytest.raises(IntegrityError): + Tag.create(name="unique") + + +def test_tag_allows_empty_string_name() -> None: + # This might be undesirable but tests current behavior + with init_db(): + tag = Tag.create(name="") + assert tag.name == "" + + +def test_work_tag_cascade_delete() -> None: + with init_db(): + work = Work.create(work="test work") + tag = Tag.create(name="test_tag") + WorkTag.create(work=work, tag=tag) + + # Delete work should cascade to WorkTag + work.delete_instance() + assert WorkTag.select().where(WorkTag.work == work.uuid).count() == 0 + + +def test_work_tag_requires_both_keys() -> None: + with init_db(): + work = Work.create(work="test") + with pytest.raises((IntegrityError, TypeError)): + WorkTag.create(work=work) + + +def test_work_with_very_long_text() -> None: + with init_db(): + long_text = "a" * 100000 + work = Work.create(work=long_text) + assert work.work == long_text + + +def test_work_with_special_characters() -> None: + with init_db(): + special_text = "Test with 🔥 emoji and\nnewlines\tand\ttabs" + work = Work.create(work=special_text) + assert work.work == special_text + + +def test_multiple_tags_per_work() -> None: + with init_db(): + work = Work.create(work="test work") + tags = [Tag.create(name=f"tag{i}") for i in range(10)] + + for tag in tags: + WorkTag.create(work=work, tag=tag) + + assert len(list(work.tags)) == 10 + + +def test_same_tag_multiple_works() -> None: + with init_db(): + tag = Tag.create(name="shared") + works = [Work.create(work=f"work{i}") for i in range(5)] + + for work in works: + WorkTag.create(work=work, tag=tag) + + assert len(list(tag.works)) == 5 diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..5e4187c --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,70 @@ +from datetime import timedelta + +from freezegun import freeze_time +import pytest + +from workedon.conf import settings +from workedon.exceptions import InvalidWorkError +from workedon.parser import InputParser +from workedon.utils import now + + +@pytest.fixture(autouse=True) +def configure_settings() -> None: + # Ensure defaults are loaded so timezone-aware parsing works in unit tests. + settings.configure() + + +def test_parse_datetime_defaults_to_now() -> None: + parser = InputParser() + assert parser.parse_datetime("") == now() + + +def test_parse_datetime_future_time_moves_to_previous_day() -> None: + with freeze_time("2024-01-02 10:00:00"): + parser = InputParser() + parsed = parser.parse_datetime("11:30pm") + assert parsed.hour == 23 + assert parsed.minute == 30 + assert parsed.date() == (now() - timedelta(days=1)).date() + + +def test_parse_duration_handles_hours_and_minutes() -> None: + parser = InputParser() + assert parser.parse_duration("[1.234h]") == 74.04 + assert parser.parse_duration("[ 45 MINs ]") == 45 + + +def test_clean_work_strips_tags_and_duration() -> None: + parser = InputParser() + cleaned = parser.clean_work(" Fix bug [30m] #dev #QA ") + assert cleaned == "Fix bug" + + +def test_parse_requires_non_empty_work_text() -> None: + parser = InputParser() + with pytest.raises(InvalidWorkError): + parser.parse("#devops #prod @ yesterday") + + +def test_parse_extracts_all_components() -> None: + parser = InputParser() + work, dt, duration, tags = parser.parse("Write docs [90m] #Dev #Docs @ yesterday") + assert work == "Write docs" + assert duration == 90 + assert tags == {"Dev", "Docs"} + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_as_datetime_returns_none_when_parser_has_no_result( + monkeypatch: pytest.MonkeyPatch, +) -> None: + parser = InputParser() + monkeypatch.setattr(parser._date_parser, "get_date_data", lambda *_: None) + assert parser._as_datetime("not a date") is None + + +def test_parse_duration_returns_none_for_unknown_unit() -> None: + parser = InputParser() + parser._DURATION_REGEX = r"\[\s*(\d+(?:\.\d+)?)\s*(sec)\s*\]" + assert parser.parse_duration("[5 sec]") is None diff --git a/tests/test_parser_edge_cases.py b/tests/test_parser_edge_cases.py new file mode 100644 index 0000000..f65b408 --- /dev/null +++ b/tests/test_parser_edge_cases.py @@ -0,0 +1,377 @@ +"""Additional edge case tests for the InputParser.""" + +from datetime import timedelta + +from freezegun import freeze_time +import pytest + +from workedon.exceptions import ( + DateTimeInFutureError, + InvalidDateTimeError, + InvalidWorkError, +) +from workedon.parser import InputParser +from workedon.utils import now + + +@pytest.fixture +def parser() -> InputParser: + return InputParser() + + +# --- Edge cases for parse_datetime --- + + +@pytest.mark.parametrize( + "input_str", + [ + "tomorrow at 5pm", + "next week", + "in 3 days", + "2099-12-31", + ], +) +def test_parse_datetime_future_raises_error(parser: InputParser, input_str: str) -> None: + with pytest.raises(DateTimeInFutureError): + parser.parse_datetime(input_str) + + +@pytest.mark.parametrize( + "input_str", + [ + "!@#$%^&*()", + "asdfghjkl", + "random gibberish", + "123abc456def", + ], +) +def test_parse_datetime_invalid_string_raises_error(parser: InputParser, input_str: str) -> None: + with pytest.raises(InvalidDateTimeError): + parser.parse_datetime(input_str) + + +def test_parse_datetime_whitespace_only_returns_now(parser: InputParser) -> None: + assert parser.parse_datetime(" ") == now() + assert parser.parse_datetime("\t\n") == now() + + +@pytest.mark.parametrize( + "input_str", + [ + "midnight", + "noon", + "3am", + "11:59pm", + "00:00", + "23:59", + ], +) +def test_parse_datetime_various_time_formats(parser: InputParser, input_str: str) -> None: + result = parser.parse_datetime(input_str) + assert result <= now() + + +def test_parse_datetime_edge_of_midnight(parser: InputParser) -> None: + with freeze_time("2024-01-15 00:01:00"): + try: + result = parser.parse_datetime("11:59pm") + except DateTimeInFutureError: + assert True + else: + assert result.hour == 23 + assert result.minute == 59 + assert result.date() == (now() - timedelta(days=1)).date() + + +@pytest.mark.parametrize( + "relative_time", + [ + "1 second ago", + "30 seconds ago", + "1 minute ago", + "59 minutes ago", + "1 hour ago", + "23 hours ago", + ], +) +def test_parse_datetime_relative_times(parser: InputParser, relative_time: str) -> None: + result = parser.parse_datetime(relative_time) + assert result <= now() + + +# --- Edge cases for parse_duration --- + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("[0.5h]", 30), + ("[0.25hr]", 15), + ("[0.1hours]", 6), + ("[1000m]", 1000), + ("[0.01h]", 0.6), + ("[99999min]", 99999), + ], +) +def test_parse_duration_edge_values(parser: InputParser, input_str: str, expected: float) -> None: + assert parser.parse_duration(input_str) == expected + + +@pytest.mark.parametrize( + "input_str", + [ + "[]", + "[h]", + "[min]", + "[hours]", + "[minutes]", + "[0h]", # Valid but zero + ], +) +def test_parse_duration_no_numeric_value(parser: InputParser, input_str: str) -> None: + result = parser.parse_duration(input_str) + assert result is None or result == 0 + + +@pytest.mark.parametrize( + "input_str", + [ + "[1.2.3h]", + "[1..5m]", + "[.5.h]", + "[-5h]", + "[+3m]", + ], +) +def test_parse_duration_malformed_numbers(parser: InputParser, input_str: str) -> None: + assert parser.parse_duration(input_str) is None + + +@pytest.mark.parametrize( + "input_str", + [ + "[3x]", + "[5d]", + "[2s]", + "[10k]", + "[1.5days]", + ], +) +def test_parse_duration_invalid_units(parser: InputParser, input_str: str) -> None: + assert parser.parse_duration(input_str) is None + + +def test_parse_duration_multiple_brackets_uses_first(parser: InputParser) -> None: + assert parser.parse_duration("[30m] [60m] [90m]") == 30 + + +def test_parse_duration_case_insensitive(parser: InputParser) -> None: + assert parser.parse_duration("[2H]") == 120 + assert parser.parse_duration("[2Hr]") == 120 + assert parser.parse_duration("[2HRS]") == 120 + assert parser.parse_duration("[30MIN]") == 30 + assert parser.parse_duration("[30Minutes]") == 30 + + +def test_parse_duration_with_spaces(parser: InputParser) -> None: + assert parser.parse_duration("[ 2 h ]") == 120 + assert parser.parse_duration("[\t30\tm\t]") == 30 + + +def test_parse_duration_no_brackets(parser: InputParser) -> None: + assert parser.parse_duration("30m") is None + assert parser.parse_duration("2h") is None + + +# --- Edge cases for parse_tags --- + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("#tag1 #tag2 #tag3", {"tag1", "tag2", "tag3"}), + ("#TAG #Tag #tag", {"TAG", "Tag", "tag"}), # Case preserved + ("#a #b #c #a #b", {"a", "b", "c"}), + ("#123 #456", {"123", "456"}), + ("#under_score #dash-tag", {"under_score", "dash-tag"}), + ("#mix123abc", {"mix123abc"}), + ], +) +def test_parse_tags_various_formats(parser: InputParser, input_str: str, expected: set) -> None: + assert parser.parse_tags(input_str) == expected + + +@pytest.mark.parametrize( + "input_str", + [ + "no tags here", + "has # space", + "##", + "###", + "#", + ], +) +def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: + assert parser.parse_tags(input_str) == set() + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("#tag!", {"tag"}), # Stops at special char + ("#tag@email", {"tag"}), + ("#tag.with.dots", {"tag"}), + ("#tag(parentheses)", {"tag"}), + ("#tag[brackets]", {"tag"}), + ("#tag{braces}", {"tag"}), + ], +) +def test_parse_tags_with_special_chars( + parser: InputParser, input_str: str, expected: set +) -> None: + assert parser.parse_tags(input_str) == expected + + +def test_parse_tags_with_unicode(parser: InputParser) -> None: + # Emoji and other unicode should not match + assert parser.parse_tags("#🔥 #😀") == set() + + +def test_parse_tags_back_to_back(parser: InputParser) -> None: + assert parser.parse_tags("#one#two#three") == {"one", "two", "three"} + + +def test_parse_tags_empty_string(parser: InputParser) -> None: + assert parser.parse_tags("") == set() + + +# --- Edge cases for clean_work --- + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("work [30m] #tag", "work"), + ("#tag1 #tag2 work", "work"), + ("work #tag [60m]", "work"), + ("[2h] #dev work #qa [30m]", "work [30m]"), + (" multiple spaces ", "multiple spaces"), + ("\ttabs\tand\nnewlines\n", "tabs and newlines"), + ], +) +def test_clean_work_removes_tags_and_duration( + parser: InputParser, input_str: str, expected: str +) -> None: + assert parser.clean_work(input_str) == expected + + +def test_clean_work_preserves_special_chars(parser: InputParser) -> None: + assert parser.clean_work("work with @mentions") == "work with @mentions" + assert parser.clean_work("work & more stuff") == "work & more stuff" + assert parser.clean_work("work (in parens)") == "work (in parens)" + + +def test_clean_work_empty_after_cleaning(parser: InputParser) -> None: + assert parser.clean_work("#tag1 #tag2 [30m]") == "" + assert parser.clean_work(" [2h] ") == "" + + +# --- Edge cases for parse (main method) --- + + +def test_parse_work_without_separator(parser: InputParser) -> None: + work, dt, duration, tags = parser.parse("simple work") + assert work == "simple work" + assert dt == now() + assert duration is None + assert tags == set() + + +def test_parse_work_with_all_components(parser: InputParser) -> None: + work, dt, duration, tags = parser.parse("complex work [90m] #dev #qa @ yesterday") + assert work == "complex work" + assert duration == 90 + assert tags == {"dev", "qa"} + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_separator_in_work_text(parser: InputParser) -> None: + # Last @ should be the separator + work, dt, duration, tags = parser.parse("email to john@example.com @ yesterday") + assert work == "email to john@example.com" + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_multiple_separators(parser: InputParser) -> None: + # Should partition on the last @ + work, dt, duration, tags = parser.parse("work @ 3pm @ yesterday") + assert work == "work @ 3pm" + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_empty_work_after_cleaning_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse("#tag [30m] @ yesterday") + + +def test_parse_separator_with_no_work_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse("@ yesterday") + + +def test_parse_whitespace_only_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse(" ") + + +def test_parse_multiple_durations_uses_first(parser: InputParser) -> None: + work, dt, duration, tags = parser.parse("work [30m] [60m] [90m]") + assert duration == 30 + + +def test_parse_preserves_work_punctuation(parser: InputParser) -> None: + work, _, _, _ = parser.parse("Work! With? Punctuation.") + assert work == "Work! With? Punctuation." + + +@pytest.mark.parametrize( + "input_str", + [ + "work @ tomorrow", + "work @ next week", + "work @ in 5 days", + ], +) +def test_parse_future_datetime_raises(parser: InputParser, input_str: str) -> None: + with pytest.raises(DateTimeInFutureError): + parser.parse(input_str) + + +def test_parse_invalid_datetime_raises(parser: InputParser) -> None: + with pytest.raises(InvalidDateTimeError): + parser.parse("work @ gibberish datetime") + + +# --- Boundary conditions --- + + +def test_parse_extremely_long_work_text(parser: InputParser) -> None: + long_text = "a" * 10000 + work, _, _, _ = parser.parse(long_text) + assert work == long_text + + +def test_parse_many_tags(parser: InputParser) -> None: + tags_str = " ".join(f"#tag{i}" for i in range(100)) + work, _, _, tags = parser.parse(f"work {tags_str}") + assert len(tags) == 100 + + +def test_parse_very_precise_duration(parser: InputParser) -> None: + work, _, duration, _ = parser.parse("work [1.123456789h]") + assert duration == 67.41 # Rounded to 2 decimals + + +def test_parse_zero_duration(parser: InputParser) -> None: + work, _, duration, _ = parser.parse("work [0h]") + assert duration == 0 diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..a5fc6e0 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,96 @@ +from datetime import datetime +import zoneinfo + +import click +import pytest + +from workedon.conf import settings +from workedon.utils import ( + add_options, + get_default_time, + get_unique_hash, + load_settings, + now, + to_internal_dt, +) + + +def test_get_unique_hash_is_hex_and_unique() -> None: + first = get_unique_hash() + second = get_unique_hash() + assert first != second + assert len(first) == 32 + int(first, 16) + + +def test_to_internal_dt_trims_seconds_and_uses_internal_tz( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(settings, "internal_tz", "UTC") + dt = datetime(2024, 1, 1, 12, 34, 56, 123456, tzinfo=zoneinfo.ZoneInfo("UTC")) + result = to_internal_dt(dt) + assert result.second == 0 + assert result.microsecond == 0 + assert result.tzinfo.key == "UTC" + + +def test_now_uses_settings_time_zone(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + result = now() + assert result.tzinfo.key == "UTC" + + +def test_get_default_time_matches_to_internal_dt( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(settings, "internal_tz", "UTC") + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + result = get_default_time() + assert result.second == 0 + assert result.microsecond == 0 + + +def test_load_settings_merges_uppercase_kwargs(monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, str] = {} + + def fake_configure(self, *, user_settings: dict[str, str] | None = None) -> None: + captured.update(user_settings or {}) + self.update(user_settings or {}) + + monkeypatch.setattr(type(settings), "configure", fake_configure) + + @load_settings + def handler(**kwargs: str) -> str: + return settings.DATE_FORMAT + + result = handler(DATE_FORMAT="%Y-%m-%d", lower="skip") # type: ignore[arg-type] + assert captured == {"DATE_FORMAT": "%Y-%m-%d"} + assert result == "%Y-%m-%d" + + +def test_load_settings_wraps_errors_in_click_exception( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(type(settings), "configure", lambda *args, **kwargs: None) + + @load_settings + def handler() -> None: + raise ValueError("boom") + + with pytest.raises(click.ClickException) as excinfo: + handler() + assert "boom" in str(excinfo.value) + + +def test_add_options_applies_click_options() -> None: + options = [ + click.option("--alpha", is_flag=True, default=False), + click.option("--beta", type=click.INT, default=1), + ] + + @add_options(options) + def handler() -> None: + return None + + assert hasattr(handler, "__click_params__") + assert {param.name for param in handler.__click_params__} == {"alpha", "beta"} diff --git a/tests/test_utils_edge_cases.py b/tests/test_utils_edge_cases.py new file mode 100644 index 0000000..efe4070 --- /dev/null +++ b/tests/test_utils_edge_cases.py @@ -0,0 +1,70 @@ +"""Edge case tests for utility functions.""" + +from datetime import datetime, timezone +import zoneinfo + +import pytest +from freezegun import freeze_time + +from workedon.conf import settings +from workedon.utils import get_unique_hash, now, to_internal_dt + + +def test_get_unique_hash_returns_32_char_hex() -> None: + hash1 = get_unique_hash() + assert len(hash1) == 32 + assert all(c in "0123456789abcdef" for c in hash1) + + +def test_get_unique_hash_generates_unique_values() -> None: + hashes = [get_unique_hash() for _ in range(1000)] + assert len(set(hashes)) == 1000 + + +def test_now_returns_timezone_aware() -> None: + current = now() + assert current.tzinfo is not None + + +def test_now_uses_configured_timezone() -> None: + settings.configure() + current = now() + assert current.tzinfo == zoneinfo.ZoneInfo(settings.TIME_ZONE) + + +def test_to_internal_dt_removes_seconds() -> None: + dt = datetime(2024, 1, 1, 12, 30, 45, 999999, tzinfo=timezone.utc) + internal = to_internal_dt(dt) + assert internal.second == 0 + assert internal.microsecond == 0 + + +def test_to_internal_dt_converts_timezone() -> None: + settings.configure() + dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + internal = to_internal_dt(dt) + assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) + + +def test_to_internal_dt_preserves_date() -> None: + dt = datetime(2024, 6, 15, 23, 59, 59, tzinfo=timezone.utc) + internal = to_internal_dt(dt) + # Date might shift due to timezone conversion + assert abs((internal.date() - dt.date()).days) <= 1 + + +def test_now_frozen_time(monkeypatch: pytest.MonkeyPatch) -> None: + settings.configure() + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + with freeze_time("2024-01-15 12:00:00"): + current = now() + assert current.hour == 12 + assert current.minute == 0 + + +def test_to_internal_dt_with_naive_datetime_raises() -> None: + settings.configure() + dt = datetime(2024, 1, 1, 12, 0, 0) # No timezone + internal = to_internal_dt(dt) + assert internal.tzinfo is not None + assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) diff --git a/tests/test_version_main.py b/tests/test_version_main.py new file mode 100644 index 0000000..709ee07 --- /dev/null +++ b/tests/test_version_main.py @@ -0,0 +1,35 @@ +import importlib +import importlib.metadata +import runpy + +import workedon +import workedon._version +import workedon.cli + + +def test_package_exports() -> None: + assert workedon.__all__ == ["__version__", "main"] + assert workedon.main is workedon.cli.main + assert isinstance(workedon.__version__, str) + assert workedon.__version__ + + +def test_version_fallback_when_package_missing(monkeypatch) -> None: + def raise_not_found(_name: str) -> str: + raise importlib.metadata.PackageNotFoundError("workedon") + + monkeypatch.setattr(importlib.metadata, "version", raise_not_found) + fallback = importlib.reload(workedon._version) + assert fallback.__version__ == "0.0.0" + importlib.reload(workedon._version) + + +def test___main__invokes_cli_main(monkeypatch) -> None: + called = {"value": False} + + def fake_main() -> None: + called["value"] = True + + monkeypatch.setattr(workedon.cli, "main", fake_main) + runpy.run_module("workedon.__main__", run_name="__main__") + assert called["value"] is True diff --git a/tests/test_workedon.py b/tests/test_workedon.py new file mode 100644 index 0000000..40b3213 --- /dev/null +++ b/tests/test_workedon.py @@ -0,0 +1,388 @@ +from __future__ import annotations + +import contextlib +from datetime import datetime, timedelta + +import click +from freezegun import freeze_time +import pytest + +from workedon import workedon +from workedon.conf import settings +from workedon.exceptions import ( + CannotFetchWorkError, + CannotSaveWorkError, + StartDateAbsentError, + StartDateGreaterError, +) +from workedon.models import Tag, Work, WorkTag, init_db + + +@pytest.fixture(autouse=True) +def configure_settings(monkeypatch: pytest.MonkeyPatch) -> None: + settings.configure() + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + monkeypatch.setattr(settings, "internal_tz", "UTC") + monkeypatch.setattr(settings, "DURATION_UNIT", "minutes") + + +class DummyWork: + def __init__(self, value: str) -> None: + self.value = value + + def __str__(self) -> str: + return self.value + + +def test_generate_work_yields_strings() -> None: + items = [DummyWork("first"), DummyWork("second")] + assert list(workedon._generate_work(iter(items))) == ["first", "second"] + + +def test_chunked_prefetch_generator_text_only() -> None: + with init_db(): + Work.create(work="alpha") + Work.create(work="beta") + + work_set = Work.select(Work.work) + output = "".join( + workedon.chunked_prefetch_generator(work_set, [Work.work], True) + ) + + assert "* alpha" in output + assert "* beta" in output + assert "id:" not in output + + +def test_chunked_prefetch_generator_prefetches_tags() -> None: + with init_db(): + work = Work.create(work="tagged entry") + tag = Tag.create(name="tag1") + WorkTag.create(work=work.uuid, tag=tag.uuid) + + work_set = Work.select(Work.uuid, Work.timestamp, Work.work, Work.duration) + output = "".join( + workedon.chunked_prefetch_generator( + work_set, + [Work.uuid, Work.timestamp, Work.work, Work.duration], + False, + ) + ) + + assert "Tags:" in output + assert "tag1" in output + + +def test_chunked_prefetch_generator_skips_missing_work( + monkeypatch: pytest.MonkeyPatch, +) -> None: + with init_db(): + Work.create(work="missing prefetch") + work_set = Work.select(Work.uuid, Work.timestamp, Work.work, Work.duration) + + monkeypatch.setattr(workedon, "prefetch", lambda *_args, **_kwargs: []) + output = list( + workedon.chunked_prefetch_generator( + work_set, + [Work.uuid, Work.timestamp, Work.work, Work.duration], + False, + ) + ) + + assert output == [] + + +def test_get_date_range_requires_start_when_end_provided() -> None: + with pytest.raises(StartDateAbsentError): + workedon._get_date_range("", "yesterday", "", None, None, None) + + +def test_get_date_range_raises_when_start_after_end() -> None: + with pytest.raises(StartDateGreaterError): + workedon._get_date_range("yesterday", "2 days ago", "", None, None, None) + + +def test_get_date_range_at_returns_single_point() -> None: + start, end = workedon._get_date_range("", "", "", None, None, "3pm yesterday") + assert start == end + + +def test_get_date_range_yesterday_period() -> None: + with freeze_time("2024-01-05 12:00:00"): + start, end = workedon._get_date_range("", "", "", "yesterday", None, None) + assert start.date() == datetime(2024, 1, 4).date() + assert end.date() == datetime(2024, 1, 4).date() + assert (end - start) >= timedelta(hours=23, minutes=59) + + +def test_get_date_range_since_uses_now_as_end() -> None: + with freeze_time("2024-01-05 12:00:00"): + start, end = workedon._get_date_range("", "", "yesterday", None, None, None) + assert start.date() == datetime(2024, 1, 4).date() + assert end.date() == datetime(2024, 1, 5).date() + + +def test_save_work_creates_tags_from_option() -> None: + workedon.save_work(("build", "feature"), ("DevOps",), "") + with init_db(): + assert Tag.select().where(Tag.name == "devops").exists() + assert WorkTag.select().count() == 1 + + +def test_save_work_raises_cannot_save_on_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def raise_error(*_args, **_kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr(workedon.Work, "create", raise_error) + with pytest.raises(CannotSaveWorkError) as excinfo: + workedon.save_work(("fail",), (), "") + assert "boom" in str(excinfo.value) + + +def test_fetch_work_raises_cannot_fetch_on_db_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + @contextlib.contextmanager + def broken_db(): + raise RuntimeError("db down") + yield # pragma: no cover + + monkeypatch.setattr(workedon, "init_db", broken_db) + with pytest.raises(CannotFetchWorkError) as excinfo: + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=False, + no_page=True, + reverse=False, + text_only=False, + tags=(), + duration="", + ) + assert "db down" in str(excinfo.value) + + +def test_fetch_work_delete_declined_keeps_data( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class DummySelect: + def where(self, *_args, **_kwargs): + return self + + def order_by(self, *_args, **_kwargs): + return self + + def limit(self, *_args, **_kwargs): + return self + + def exists(self) -> bool: + return True + + deleted = {"called": False} + + def fake_delete(): + deleted["called"] = True + + class DummyDelete: + def where(self, *_args, **_kwargs): + return self + + def execute(self) -> int: + return 0 + + return DummyDelete() + + @contextlib.contextmanager + def fake_db(): + yield None + + monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) + monkeypatch.setattr(workedon.Work, "delete", fake_delete) + monkeypatch.setattr(workedon, "init_db", fake_db) + monkeypatch.setattr(click, "confirm", lambda *_args, **_kwargs: False) + + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=True, + no_page=True, + reverse=False, + text_only=False, + tags=(), + duration="", + ) + + assert deleted["called"] is False + + +def test_fetch_work_uses_pager_for_multiple_rows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, list[str]] = {} + + def fake_pager(gen) -> None: + captured["output"] = list(gen) + + class DummySelect: + def where(self, *_args, **_kwargs): + return self + + def order_by(self, *_args, **_kwargs): + return self + + def limit(self, *_args, **_kwargs): + return self + + def exists(self) -> bool: + return True + + def count(self) -> int: + return 2 + + @contextlib.contextmanager + def fake_db(): + yield None + + def fake_generator(*_args, **_kwargs): + return iter(["id: first\n", "id: second\n"]) + + monkeypatch.setattr(click, "echo_via_pager", fake_pager) + monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) + monkeypatch.setattr(workedon, "chunked_prefetch_generator", fake_generator) + monkeypatch.setattr(workedon, "init_db", fake_db) + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=False, + no_page=False, + reverse=False, + text_only=False, + tags=(), + duration="", + ) + + assert any("id:" in line for line in captured.get("output", [])) + + +def test_fetch_tags_returns_saved_tags(monkeypatch: pytest.MonkeyPatch) -> None: + class DummyTag: + name = "alpha" + + monkeypatch.setattr(workedon.Tag, "select", lambda *_args, **_kwargs: [DummyTag()]) + tags = [tag.name for tag in workedon.fetch_tags()] + assert tags == ["alpha"] + + +def test_fetch_work_rejects_invalid_duration_filter( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(workedon.re, "match", lambda *_args, **_kwargs: None) + with pytest.raises(CannotFetchWorkError) as excinfo: + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=False, + no_page=True, + reverse=False, + text_only=False, + tags=(), + duration="n/a", + ) + assert "Invalid duration filter" in str(excinfo.value) + + +def test_fetch_work_rejects_invalid_duration_operator( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class DummyMatch: + def groups(self) -> tuple[str, str]: + return "?!", "5m" + + monkeypatch.setattr(workedon.re, "match", lambda *_args, **_kwargs: DummyMatch()) + with pytest.raises(CannotFetchWorkError) as excinfo: + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=False, + no_page=True, + reverse=False, + text_only=False, + tags=(), + duration="?!5m", + ) + assert "Invalid duration operator" in str(excinfo.value) + + +def test_fetch_work_skips_date_filter_when_range_is_none( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class DummySelect: + def where(self, *_args, **_kwargs): + return self + + def order_by(self, *_args, **_kwargs): + return self + + def limit(self, *_args, **_kwargs): + return self + + def exists(self) -> bool: + return False + + @contextlib.contextmanager + def fake_db(): + yield None + + monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) + monkeypatch.setattr(workedon, "_get_date_range", lambda *_args, **_kwargs: (None, None)) + monkeypatch.setattr(workedon, "init_db", fake_db) + monkeypatch.setattr(click, "echo", lambda *_args, **_kwargs: None) + + workedon.fetch_work( + count=None, + work_id="", + start_date="", + end_date="", + since="", + period=None, + on=None, + at=None, + delete=False, + no_page=True, + reverse=False, + text_only=False, + tags=(), + duration="", + ) From 24345c4fdc46df56aa8baf3716637617e2774d0a Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 23 Dec 2025 22:52:54 -0500 Subject: [PATCH 15/33] fix ruff warnings --- tests/test_cli_branches.py | 3 +-- tests/test_conf.py | 16 ++++------------ tests/test_integration.py | 8 ++------ tests/test_models.py | 21 ++++++--------------- tests/test_models_edge_cases.py | 12 +++++------- tests/test_parser_edge_cases.py | 16 +++++++--------- tests/test_utils_edge_cases.py | 2 +- tests/test_workedon.py | 5 +---- 8 files changed, 27 insertions(+), 56 deletions(-) diff --git a/tests/test_cli_branches.py b/tests/test_cli_branches.py index aad5451..8151f8b 100644 --- a/tests/test_cli_branches.py +++ b/tests/test_cli_branches.py @@ -1,10 +1,9 @@ import importlib -import os import pytest -from workedon import cli import workedon +from workedon import cli def test_cli_import_skips_warning_filter_in_debug(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/test_conf.py b/tests/test_conf.py index 362d130..2a991a2 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -15,9 +15,7 @@ def test_settings_getattr_and_setattr() -> None: assert settings["FOO"] == "bar" -def test_configure_creates_settings_file( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_configure_creates_settings_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: conf_path = tmp_path / "wonfile.py" monkeypatch.setattr(conf, "CONF_PATH", conf_path) @@ -28,9 +26,7 @@ def test_configure_creates_settings_file( assert SETTINGS_HEADER.strip() in conf_path.read_text() -def test_configure_loads_user_settings( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_configure_loads_user_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: conf_path = tmp_path / "wonfile.py" conf_path.write_text('TIME_FORMAT = "%H:%M"\n') monkeypatch.setattr(conf, "CONF_PATH", conf_path) @@ -41,9 +37,7 @@ def test_configure_loads_user_settings( assert settings.TIME_FORMAT == "%H:%M" -def test_configure_merges_user_settings( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_configure_merges_user_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: conf_path = tmp_path / "wonfile.py" conf_path.write_text('DATE_FORMAT = "%Y"\n') monkeypatch.setattr(conf, "CONF_PATH", conf_path) @@ -54,9 +48,7 @@ def test_configure_merges_user_settings( assert settings.DATE_FORMAT == "%d" -def test_configure_raises_on_bad_spec( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_configure_raises_on_bad_spec(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: conf_path = tmp_path / "wonfile.py" conf_path.write_text("# ok\n") monkeypatch.setattr(conf, "CONF_PATH", conf_path) diff --git a/tests/test_integration.py b/tests/test_integration.py index c4e6246..3456886 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -28,9 +28,7 @@ def test_full_workflow_save_modify_fetch_delete(runner: CliRunner) -> None: assert "working on feature" in duration_result.output # Delete - delete_result = runner.invoke( - cli.what, ["--no-page", "--tag", "dev", "--delete"], input="y" - ) + delete_result = runner.invoke(cli.what, ["--no-page", "--tag", "dev", "--delete"], input="y") assert delete_result.exit_code == 0 assert "deleted successfully" in delete_result.output @@ -66,9 +64,7 @@ def test_duration_with_timezone_changes(runner: CliRunner) -> None: runner.invoke(cli.main, ["work [90m] @ 3pm yesterday", "--time-zone", "UTC"]) # Fetch in different timezone - result = runner.invoke( - cli.what, ["--no-page", "--last", "--time-zone", "Asia/Tokyo"] - ) + result = runner.invoke(cli.what, ["--no-page", "--last", "--time-zone", "Asia/Tokyo"]) assert result.exit_code == 0 assert "Duration:" in result.output diff --git a/tests/test_models.py b/tests/test_models.py index 815927a..7739c5b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -17,9 +17,7 @@ def configure_settings(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "internal_tz", "UTC") -def test_get_or_create_db_creates_file( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_get_or_create_db_creates_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: db_path = tmp_path / "won.db" monkeypatch.setattr(models, "DB_PATH", db_path) @@ -69,9 +67,7 @@ def test_apply_pending_migrations_from_zero(tmp_path: Path) -> None: db.close() -def test_apply_pending_migrations_from_v1( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_apply_pending_migrations_from_v1(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: db = SqliteDatabase(str(tmp_path / "v1-pending.db")) db.connect() try: @@ -102,9 +98,7 @@ def test_apply_pending_migrations_from_v2(tmp_path: Path) -> None: db.execute_sql( "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" ) - db.execute_sql( - "CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);" - ) + db.execute_sql("CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);") db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") models._set_db_user_version(db, 2) with db.bind_ctx([Work, Tag, WorkTag]): @@ -143,9 +137,7 @@ def test_migrate_v2_to_v3_adds_duration_index(tmp_path: Path) -> None: db.execute_sql( "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" ) - db.execute_sql( - "CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);" - ) + db.execute_sql("CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);") db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") models._set_db_user_version(db, 2) with db.bind_ctx([Work, Tag, WorkTag]): @@ -165,9 +157,7 @@ def test_apply_pending_migrations_raises_on_mismatch( db.connect() try: with db.bind_ctx([Work, Tag, WorkTag]): - monkeypatch.setattr( - models, "get_db_user_version", lambda *_: CURRENT_DB_VERSION + 1 - ) + monkeypatch.setattr(models, "get_db_user_version", lambda *_: CURRENT_DB_VERSION + 1) with pytest.raises(DBInitializationError): models._apply_pending_migrations(db) finally: @@ -181,6 +171,7 @@ def test_apply_pending_migrations_wraps_operational_error( db.connect() try: with db.bind_ctx([Work, Tag, WorkTag]): + def raise_operational_error(*_args, **_kwargs): raise OperationalError("fail") diff --git a/tests/test_models_edge_cases.py b/tests/test_models_edge_cases.py index 0b2e90d..b201fd2 100644 --- a/tests/test_models_edge_cases.py +++ b/tests/test_models_edge_cases.py @@ -1,21 +1,19 @@ """Edge case tests for database models.""" -import pytest from peewee import IntegrityError +import pytest from workedon.models import Tag, Work, WorkTag, init_db def test_work_requires_uuid() -> None: - with init_db(): - with pytest.raises(IntegrityError): - Work.create(uuid=None, work="test") + with init_db(), pytest.raises(IntegrityError): + Work.create(uuid=None, work="test") def test_work_requires_work_text() -> None: - with init_db(): - with pytest.raises(IntegrityError): - Work.create(work=None) + with init_db(), pytest.raises(IntegrityError): + Work.create(work=None) def test_work_allows_null_duration() -> None: diff --git a/tests/test_parser_edge_cases.py b/tests/test_parser_edge_cases.py index f65b408..918b522 100644 --- a/tests/test_parser_edge_cases.py +++ b/tests/test_parser_edge_cases.py @@ -226,9 +226,7 @@ def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: ("#tag{braces}", {"tag"}), ], ) -def test_parse_tags_with_special_chars( - parser: InputParser, input_str: str, expected: set -) -> None: +def test_parse_tags_with_special_chars(parser: InputParser, input_str: str, expected: set) -> None: assert parser.parse_tags(input_str) == expected @@ -297,14 +295,14 @@ def test_parse_work_with_all_components(parser: InputParser) -> None: def test_parse_separator_in_work_text(parser: InputParser) -> None: # Last @ should be the separator - work, dt, duration, tags = parser.parse("email to john@example.com @ yesterday") + work, dt, _duration, _tags = parser.parse("email to john@example.com @ yesterday") assert work == "email to john@example.com" assert dt.date() == (now() - timedelta(days=1)).date() def test_parse_multiple_separators(parser: InputParser) -> None: # Should partition on the last @ - work, dt, duration, tags = parser.parse("work @ 3pm @ yesterday") + work, dt, _duration, _tags = parser.parse("work @ 3pm @ yesterday") assert work == "work @ 3pm" assert dt.date() == (now() - timedelta(days=1)).date() @@ -325,7 +323,7 @@ def test_parse_whitespace_only_raises(parser: InputParser) -> None: def test_parse_multiple_durations_uses_first(parser: InputParser) -> None: - work, dt, duration, tags = parser.parse("work [30m] [60m] [90m]") + _work, _dt, duration, _tags = parser.parse("work [30m] [60m] [90m]") assert duration == 30 @@ -363,15 +361,15 @@ def test_parse_extremely_long_work_text(parser: InputParser) -> None: def test_parse_many_tags(parser: InputParser) -> None: tags_str = " ".join(f"#tag{i}" for i in range(100)) - work, _, _, tags = parser.parse(f"work {tags_str}") + _work, _, _, tags = parser.parse(f"work {tags_str}") assert len(tags) == 100 def test_parse_very_precise_duration(parser: InputParser) -> None: - work, _, duration, _ = parser.parse("work [1.123456789h]") + _work, _, duration, _ = parser.parse("work [1.123456789h]") assert duration == 67.41 # Rounded to 2 decimals def test_parse_zero_duration(parser: InputParser) -> None: - work, _, duration, _ = parser.parse("work [0h]") + _work, _, duration, _ = parser.parse("work [0h]") assert duration == 0 diff --git a/tests/test_utils_edge_cases.py b/tests/test_utils_edge_cases.py index efe4070..9d53a54 100644 --- a/tests/test_utils_edge_cases.py +++ b/tests/test_utils_edge_cases.py @@ -3,8 +3,8 @@ from datetime import datetime, timezone import zoneinfo -import pytest from freezegun import freeze_time +import pytest from workedon.conf import settings from workedon.utils import get_unique_hash, now, to_internal_dt diff --git a/tests/test_workedon.py b/tests/test_workedon.py index 40b3213..e1a250d 100644 --- a/tests/test_workedon.py +++ b/tests/test_workedon.py @@ -45,9 +45,7 @@ def test_chunked_prefetch_generator_text_only() -> None: Work.create(work="beta") work_set = Work.select(Work.work) - output = "".join( - workedon.chunked_prefetch_generator(work_set, [Work.work], True) - ) + output = "".join(workedon.chunked_prefetch_generator(work_set, [Work.work], True)) assert "* alpha" in output assert "* beta" in output @@ -147,7 +145,6 @@ def test_fetch_work_raises_cannot_fetch_on_db_failure( @contextlib.contextmanager def broken_db(): raise RuntimeError("db down") - yield # pragma: no cover monkeypatch.setattr(workedon, "init_db", broken_db) with pytest.raises(CannotFetchWorkError) as excinfo: From 1ebc442a546c229e9b8fedfc376fc20a3c4822b4 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 18:00:49 -0500 Subject: [PATCH 16/33] remove unnecessary guard and test --- tests/test_workedon.py | 42 ------------------------------------------ workedon/workedon.py | 3 +-- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/tests/test_workedon.py b/tests/test_workedon.py index e1a250d..fa0eed4 100644 --- a/tests/test_workedon.py +++ b/tests/test_workedon.py @@ -341,45 +341,3 @@ def groups(self) -> tuple[str, str]: ) assert "Invalid duration operator" in str(excinfo.value) - -def test_fetch_work_skips_date_filter_when_range_is_none( - monkeypatch: pytest.MonkeyPatch, -) -> None: - class DummySelect: - def where(self, *_args, **_kwargs): - return self - - def order_by(self, *_args, **_kwargs): - return self - - def limit(self, *_args, **_kwargs): - return self - - def exists(self) -> bool: - return False - - @contextlib.contextmanager - def fake_db(): - yield None - - monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) - monkeypatch.setattr(workedon, "_get_date_range", lambda *_args, **_kwargs: (None, None)) - monkeypatch.setattr(workedon, "init_db", fake_db) - monkeypatch.setattr(click, "echo", lambda *_args, **_kwargs: None) - - workedon.fetch_work( - count=None, - work_id="", - start_date="", - end_date="", - since="", - period=None, - on=None, - at=None, - delete=False, - no_page=True, - reverse=False, - text_only=False, - tags=(), - duration="", - ) diff --git a/workedon/workedon.py b/workedon/workedon.py index f39165d..a1e528e 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -198,8 +198,7 @@ def fetch_work( work_set = work_set.where(op_map[comp_op](Work.duration, minutes)) # date range start, end = _get_date_range(start_date, end_date, since, period, on, at) - if start and end: - work_set = work_set.where((Work.timestamp >= start) & (Work.timestamp <= end)) + work_set = work_set.where((Work.timestamp >= start) & (Work.timestamp <= end)) # order sort_order = Work.timestamp.asc() if reverse else Work.timestamp.desc() work_set = work_set.order_by(sort_order) From c55ed4616a87773e4990008289cae88b2211d608 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 18:06:48 -0500 Subject: [PATCH 17/33] Update test_integration.py --- tests/test_integration.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 3456886..de9ba35 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,10 @@ """Integration tests covering complete workflows.""" +from datetime import datetime +import zoneinfo + from click.testing import CliRunner +from freezegun import freeze_time import pytest from workedon import cli @@ -92,16 +96,20 @@ def test_edge_case_midnight_boundary(runner: CliRunner) -> None: assert "midnight task" in today_result.output +@freeze_time("2024-01-10 15:00:00") def test_complex_datetime_parsing(runner: CliRunner) -> None: + fmt = "%Y-%m-%d %H:%M %z" + tz = zoneinfo.ZoneInfo("UTC") test_cases = [ - "meeting @ 3pm last friday", - "call @ 9:30am yesterday", - "email @ noon 3 days ago", - "standup @ 10am this week", + ("meeting @ 3pm 5 days ago", datetime(2024, 1, 5, 15, 0, tzinfo=tz)), + ("call @ 9:30am yesterday", datetime(2024, 1, 9, 9, 30, tzinfo=tz)), + ("email @ noon 3 days ago", datetime(2024, 1, 7, 12, 0, tzinfo=tz)), + ("standup @ 10am this week", datetime(2024, 1, 10, 10, 0, tzinfo=tz)), ] + env = {"WORKEDON_TIME_ZONE": "UTC", "WORKEDON_DATETIME_FORMAT": fmt} - for case in test_cases: - result = runner.invoke(cli.main, case.split()) - assert result.exit_code == 0 or any( - keyword in result.output.lower() for keyword in ("future", "invalid") - ) + for case, expected_dt in test_cases: + result = runner.invoke(cli.main, case.split(), env=env) + assert result.exit_code == 0 + assert "Work saved." in result.output + assert f"Date: {expected_dt.strftime(fmt)}" in result.output From e9699ab9b42670d8e30e15b760450cc8caad526c Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 18:09:17 -0500 Subject: [PATCH 18/33] Update test_integration.py --- tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index de9ba35..2745ae2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -54,7 +54,7 @@ def test_multiple_tags_filtering(runner: CliRunner) -> None: assert "task2" in dev_result.output assert "task3" not in dev_result.output - # Filter by multiple tags (AND logic) + # Filter by multiple tags (OR logic) multi_result = runner.invoke( cli.what, ["--no-page", "--tag", "dev", "--tag", "frontend", "--yesterday"] ) From 3b104f7e3fb4feb676a244d9fb8cef40aab24485 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 18:16:26 -0500 Subject: [PATCH 19/33] fix lint --- tests/test_models.py | 14 ++++--- tests/test_parser.py | 10 ++++- tests/test_utils.py | 22 +++++++---- tests/test_version_main.py | 6 ++- tests/test_workedon.py | 76 +++++++++++++++++++++++--------------- 5 files changed, 83 insertions(+), 45 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 7739c5b..2530b88 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -72,7 +72,8 @@ def test_apply_pending_migrations_from_v1(tmp_path: Path, monkeypatch: pytest.Mo db.connect() try: db.execute_sql( - "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME);" + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, " + "timestamp DATETIME);" ) models._set_db_user_version(db, 1) @@ -96,7 +97,8 @@ def test_apply_pending_migrations_from_v2(tmp_path: Path) -> None: db.connect() try: db.execute_sql( - "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, " + "timestamp DATETIME, duration REAL);" ) db.execute_sql("CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);") db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") @@ -116,7 +118,8 @@ def test_migrate_v1_to_v2_adds_tables_and_duration(tmp_path: Path) -> None: db.connect() try: db.execute_sql( - "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME);" + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, " + "timestamp DATETIME);" ) models._set_db_user_version(db, 1) with db.bind_ctx([Work, Tag, WorkTag]): @@ -135,7 +138,8 @@ def test_migrate_v2_to_v3_adds_duration_index(tmp_path: Path) -> None: db.connect() try: db.execute_sql( - "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, timestamp DATETIME, duration REAL);" + "CREATE TABLE work (uuid TEXT PRIMARY KEY, created DATETIME, work TEXT, " + "timestamp DATETIME, duration REAL);" ) db.execute_sql("CREATE TABLE tag (uuid TEXT PRIMARY KEY, name TEXT, created DATETIME);") db.execute_sql("CREATE TABLE work_tag (work TEXT, tag TEXT);") @@ -172,7 +176,7 @@ def test_apply_pending_migrations_wraps_operational_error( try: with db.bind_ctx([Work, Tag, WorkTag]): - def raise_operational_error(*_args, **_kwargs): + def raise_operational_error(*_args: object, **_kwargs: object) -> None: raise OperationalError("fail") monkeypatch.setattr(db, "execute_sql", raise_operational_error) diff --git a/tests/test_parser.py b/tests/test_parser.py index 5e4187c..3e9a903 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -64,7 +64,13 @@ def test_as_datetime_returns_none_when_parser_has_no_result( assert parser._as_datetime("not a date") is None -def test_parse_duration_returns_none_for_unknown_unit() -> None: +def test_parse_duration_returns_none_for_unknown_unit( + monkeypatch: pytest.MonkeyPatch, +) -> None: parser = InputParser() - parser._DURATION_REGEX = r"\[\s*(\d+(?:\.\d+)?)\s*(sec)\s*\]" + monkeypatch.setattr( + parser, + "_DURATION_REGEX", + r"\[\s*(\d+(?:\.\d+)?)\s*(sec)\s*\]", + ) assert parser.parse_duration("[5 sec]") is None diff --git a/tests/test_utils.py b/tests/test_utils.py index a5fc6e0..fb96af5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,11 @@ from datetime import datetime +from typing import cast import zoneinfo import click import pytest -from workedon.conf import settings +from workedon.conf import Settings, settings from workedon.utils import ( add_options, get_default_time, @@ -31,13 +32,17 @@ def test_to_internal_dt_trims_seconds_and_uses_internal_tz( result = to_internal_dt(dt) assert result.second == 0 assert result.microsecond == 0 - assert result.tzinfo.key == "UTC" + tzinfo = result.tzinfo + assert isinstance(tzinfo, zoneinfo.ZoneInfo) + assert tzinfo.key == "UTC" def test_now_uses_settings_time_zone(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "TIME_ZONE", "UTC") result = now() - assert result.tzinfo.key == "UTC" + tzinfo = result.tzinfo + assert isinstance(tzinfo, zoneinfo.ZoneInfo) + assert tzinfo.key == "UTC" def test_get_default_time_matches_to_internal_dt( @@ -53,7 +58,7 @@ def test_get_default_time_matches_to_internal_dt( def test_load_settings_merges_uppercase_kwargs(monkeypatch: pytest.MonkeyPatch) -> None: captured: dict[str, str] = {} - def fake_configure(self, *, user_settings: dict[str, str] | None = None) -> None: + def fake_configure(self: Settings, *, user_settings: dict[str, str] | None = None) -> None: captured.update(user_settings or {}) self.update(user_settings or {}) @@ -61,9 +66,9 @@ def fake_configure(self, *, user_settings: dict[str, str] | None = None) -> None @load_settings def handler(**kwargs: str) -> str: - return settings.DATE_FORMAT + return cast(str, settings.DATE_FORMAT) - result = handler(DATE_FORMAT="%Y-%m-%d", lower="skip") # type: ignore[arg-type] + result = handler(DATE_FORMAT="%Y-%m-%d", lower="skip") assert captured == {"DATE_FORMAT": "%Y-%m-%d"} assert result == "%Y-%m-%d" @@ -71,7 +76,10 @@ def handler(**kwargs: str) -> str: def test_load_settings_wraps_errors_in_click_exception( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(type(settings), "configure", lambda *args, **kwargs: None) + def noop_configure(*_args: object, **_kwargs: object) -> None: + return None + + monkeypatch.setattr(type(settings), "configure", noop_configure) @load_settings def handler() -> None: diff --git a/tests/test_version_main.py b/tests/test_version_main.py index 709ee07..d319e4e 100644 --- a/tests/test_version_main.py +++ b/tests/test_version_main.py @@ -2,6 +2,8 @@ import importlib.metadata import runpy +import pytest + import workedon import workedon._version import workedon.cli @@ -14,7 +16,7 @@ def test_package_exports() -> None: assert workedon.__version__ -def test_version_fallback_when_package_missing(monkeypatch) -> None: +def test_version_fallback_when_package_missing(monkeypatch: pytest.MonkeyPatch) -> None: def raise_not_found(_name: str) -> str: raise importlib.metadata.PackageNotFoundError("workedon") @@ -24,7 +26,7 @@ def raise_not_found(_name: str) -> str: importlib.reload(workedon._version) -def test___main__invokes_cli_main(monkeypatch) -> None: +def test___main__invokes_cli_main(monkeypatch: pytest.MonkeyPatch) -> None: called = {"value": False} def fake_main() -> None: diff --git a/tests/test_workedon.py b/tests/test_workedon.py index fa0eed4..132211d 100644 --- a/tests/test_workedon.py +++ b/tests/test_workedon.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections.abc import Generator, Iterable, Iterator import contextlib from datetime import datetime, timedelta +from typing import cast import click from freezegun import freeze_time @@ -36,7 +38,8 @@ def __str__(self) -> str: def test_generate_work_yields_strings() -> None: items = [DummyWork("first"), DummyWork("second")] - assert list(workedon._generate_work(iter(items))) == ["first", "second"] + work_iter = cast(Iterator[Work], iter(items)) + assert list(workedon._generate_work(work_iter)) == ["first", "second"] def test_chunked_prefetch_generator_text_only() -> None: @@ -130,7 +133,7 @@ def test_save_work_creates_tags_from_option() -> None: def test_save_work_raises_cannot_save_on_failure( monkeypatch: pytest.MonkeyPatch, ) -> None: - def raise_error(*_args, **_kwargs): + def raise_error(*_args: object, **_kwargs: object) -> None: raise RuntimeError("boom") monkeypatch.setattr(workedon.Work, "create", raise_error) @@ -142,11 +145,18 @@ def raise_error(*_args, **_kwargs): def test_fetch_work_raises_cannot_fetch_on_db_failure( monkeypatch: pytest.MonkeyPatch, ) -> None: - @contextlib.contextmanager - def broken_db(): - raise RuntimeError("db down") + class DBDownError(RuntimeError): + def __init__(self) -> None: + super().__init__("db down") + + class BrokenDB: + def __enter__(self) -> None: + raise DBDownError() - monkeypatch.setattr(workedon, "init_db", broken_db) + def __exit__(self, *_args: object) -> None: + return None + + monkeypatch.setattr(workedon, "init_db", lambda: BrokenDB()) with pytest.raises(CannotFetchWorkError) as excinfo: workedon.fetch_work( count=None, @@ -171,40 +181,45 @@ def test_fetch_work_delete_declined_keeps_data( monkeypatch: pytest.MonkeyPatch, ) -> None: class DummySelect: - def where(self, *_args, **_kwargs): + def where(self, *_args: object, **_kwargs: object) -> DummySelect: return self - def order_by(self, *_args, **_kwargs): + def order_by(self, *_args: object, **_kwargs: object) -> DummySelect: return self - def limit(self, *_args, **_kwargs): + def limit(self, *_args: object, **_kwargs: object) -> DummySelect: return self def exists(self) -> bool: return True - deleted = {"called": False} - - def fake_delete(): - deleted["called"] = True + class DummyDelete: + def where(self, *_args: object, **_kwargs: object) -> DummyDelete: + return self - class DummyDelete: - def where(self, *_args, **_kwargs): - return self + def execute(self) -> int: + return 0 - def execute(self) -> int: - return 0 + deleted = {"called": False} + def fake_delete() -> DummyDelete: + deleted["called"] = True return DummyDelete() @contextlib.contextmanager - def fake_db(): + def fake_db() -> Generator[None, None, None]: yield None - monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) + def fake_select(*_args: object, **_kwargs: object) -> DummySelect: + return DummySelect() + + def fake_confirm(*_args: object, **_kwargs: object) -> bool: + return False + + monkeypatch.setattr(workedon.Work, "select", fake_select) monkeypatch.setattr(workedon.Work, "delete", fake_delete) monkeypatch.setattr(workedon, "init_db", fake_db) - monkeypatch.setattr(click, "confirm", lambda *_args, **_kwargs: False) + monkeypatch.setattr(click, "confirm", fake_confirm) workedon.fetch_work( count=None, @@ -231,17 +246,17 @@ def test_fetch_work_uses_pager_for_multiple_rows( ) -> None: captured: dict[str, list[str]] = {} - def fake_pager(gen) -> None: + def fake_pager(gen: Iterable[str]) -> None: captured["output"] = list(gen) class DummySelect: - def where(self, *_args, **_kwargs): + def where(self, *_args: object, **_kwargs: object) -> DummySelect: return self - def order_by(self, *_args, **_kwargs): + def order_by(self, *_args: object, **_kwargs: object) -> DummySelect: return self - def limit(self, *_args, **_kwargs): + def limit(self, *_args: object, **_kwargs: object) -> DummySelect: return self def exists(self) -> bool: @@ -251,14 +266,18 @@ def count(self) -> int: return 2 @contextlib.contextmanager - def fake_db(): + def fake_db() -> Generator[None, None, None]: yield None - def fake_generator(*_args, **_kwargs): + def fake_generator(*_args: object, **_kwargs: object) -> Iterator[str]: return iter(["id: first\n", "id: second\n"]) monkeypatch.setattr(click, "echo_via_pager", fake_pager) - monkeypatch.setattr(workedon.Work, "select", lambda *_args, **_kwargs: DummySelect()) + + def fake_select(*_args: object, **_kwargs: object) -> DummySelect: + return DummySelect() + + monkeypatch.setattr(workedon.Work, "select", fake_select) monkeypatch.setattr(workedon, "chunked_prefetch_generator", fake_generator) monkeypatch.setattr(workedon, "init_db", fake_db) workedon.fetch_work( @@ -340,4 +359,3 @@ def groups(self) -> tuple[str, str]: duration="?!5m", ) assert "Invalid duration operator" in str(excinfo.value) - From 780be7b0132ffdee471d2ff231ab783574f10168 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 19:44:16 -0500 Subject: [PATCH 20/33] cleanup --- tests/test_cli.py | 4 - tests/test_models.py | 116 +++++++++- tests/test_models_edge_cases.py | 117 ---------- tests/test_parser.py | 351 +++++++++++++++++++++++++++++- tests/test_parser_edge_cases.py | 375 -------------------------------- tests/test_utils.py | 43 +++- tests/test_utils_edge_cases.py | 70 ------ 7 files changed, 507 insertions(+), 569 deletions(-) delete mode 100644 tests/test_models_edge_cases.py delete mode 100644 tests/test_parser_edge_cases.py delete mode 100644 tests/test_utils_edge_cases.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a4c43e..64b9398 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -220,10 +220,6 @@ def test_timezone_option( ("learning to cook @ 3pm yesterday", ["-f", "2 days ago", "-t", "3:05pm yesterday"]), # 36 ("watching tv @ 9am", ["-g"]), # 37 ("taking wife shopping @ 3pm", ["--no-page"]), # 38 - ( - "weights at the gym", - ["--count", "1"], - ), ], ) def test_save_and_fetch_others(runner: CliRunner, command: str, flag: list[str]) -> None: diff --git a/tests/test_models.py b/tests/test_models.py index 2530b88..88969ac 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,6 @@ from pathlib import Path -from peewee import OperationalError, SqliteDatabase +from peewee import IntegrityError, OperationalError, SqliteDatabase import pytest from workedon import models @@ -206,3 +206,117 @@ def test_tag_str_and_work_text_only_str() -> None: work = Work(uuid=None, work="text only") assert "* text only" in str(work) + + +# ---edge-cases--- + + +def test_work_requires_uuid() -> None: + with init_db(), pytest.raises(IntegrityError): + Work.create(uuid=None, work="test") + + +def test_work_requires_work_text() -> None: + with init_db(), pytest.raises(IntegrityError): + Work.create(work=None) + + +def test_work_allows_null_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=None) + assert work.duration is None + + +def test_work_allows_zero_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=0) + assert work.duration == 0 + + +def test_work_allows_large_duration() -> None: + with init_db(): + work = Work.create(work="test work", duration=999999.99) + assert work.duration == 999999.99 + + +def test_work_string_representation_with_no_tags() -> None: + with init_db(): + work = Work.create(work="simple work") + output = str(work) + assert "simple work" in output + assert "Tags:" not in output + + +def test_work_string_representation_with_no_duration() -> None: + with init_db(): + work = Work.create(work="simple work", duration=None) + output = str(work) + assert "Duration:" not in output + + +def test_tag_requires_unique_name() -> None: + with init_db(): + Tag.create(name="unique") + with pytest.raises(IntegrityError): + Tag.create(name="unique") + + +def test_tag_allows_empty_string_name() -> None: + # This might be undesirable but tests current behavior + with init_db(): + tag = Tag.create(name="") + assert tag.name == "" + + +def test_work_tag_cascade_delete() -> None: + with init_db(): + work = Work.create(work="test work") + tag = Tag.create(name="test_tag") + WorkTag.create(work=work, tag=tag) + + # Delete work should cascade to WorkTag + work.delete_instance() + assert WorkTag.select().where(WorkTag.work == work.uuid).count() == 0 + + +def test_work_tag_requires_both_keys() -> None: + with init_db(): + work = Work.create(work="test") + with pytest.raises((IntegrityError, TypeError)): + WorkTag.create(work=work) + + +def test_work_with_very_long_text() -> None: + with init_db(): + long_text = "a" * 100000 + work = Work.create(work=long_text) + assert work.work == long_text + + +def test_work_with_special_characters() -> None: + with init_db(): + special_text = "Test with symbols !@# and\nnewlines\tand\ttabs" + work = Work.create(work=special_text) + assert work.work == special_text + + +def test_multiple_tags_per_work() -> None: + with init_db(): + work = Work.create(work="test work") + tags = [Tag.create(name=f"tag{i}") for i in range(10)] + + for tag in tags: + WorkTag.create(work=work, tag=tag) + + assert len(list(work.tags)) == 10 + + +def test_same_tag_multiple_works() -> None: + with init_db(): + tag = Tag.create(name="shared") + works = [Work.create(work=f"work{i}") for i in range(5)] + + for work in works: + WorkTag.create(work=work, tag=tag) + + assert len(list(tag.works)) == 5 diff --git a/tests/test_models_edge_cases.py b/tests/test_models_edge_cases.py deleted file mode 100644 index b201fd2..0000000 --- a/tests/test_models_edge_cases.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Edge case tests for database models.""" - -from peewee import IntegrityError -import pytest - -from workedon.models import Tag, Work, WorkTag, init_db - - -def test_work_requires_uuid() -> None: - with init_db(), pytest.raises(IntegrityError): - Work.create(uuid=None, work="test") - - -def test_work_requires_work_text() -> None: - with init_db(), pytest.raises(IntegrityError): - Work.create(work=None) - - -def test_work_allows_null_duration() -> None: - with init_db(): - work = Work.create(work="test work", duration=None) - assert work.duration is None - - -def test_work_allows_zero_duration() -> None: - with init_db(): - work = Work.create(work="test work", duration=0) - assert work.duration == 0 - - -def test_work_allows_large_duration() -> None: - with init_db(): - work = Work.create(work="test work", duration=999999.99) - assert work.duration == 999999.99 - - -def test_work_string_representation_with_no_tags() -> None: - with init_db(): - work = Work.create(work="simple work") - output = str(work) - assert "simple work" in output - assert "Tags:" not in output - - -def test_work_string_representation_with_no_duration() -> None: - with init_db(): - work = Work.create(work="simple work", duration=None) - output = str(work) - assert "Duration:" not in output - - -def test_tag_requires_unique_name() -> None: - with init_db(): - Tag.create(name="unique") - with pytest.raises(IntegrityError): - Tag.create(name="unique") - - -def test_tag_allows_empty_string_name() -> None: - # This might be undesirable but tests current behavior - with init_db(): - tag = Tag.create(name="") - assert tag.name == "" - - -def test_work_tag_cascade_delete() -> None: - with init_db(): - work = Work.create(work="test work") - tag = Tag.create(name="test_tag") - WorkTag.create(work=work, tag=tag) - - # Delete work should cascade to WorkTag - work.delete_instance() - assert WorkTag.select().where(WorkTag.work == work.uuid).count() == 0 - - -def test_work_tag_requires_both_keys() -> None: - with init_db(): - work = Work.create(work="test") - with pytest.raises((IntegrityError, TypeError)): - WorkTag.create(work=work) - - -def test_work_with_very_long_text() -> None: - with init_db(): - long_text = "a" * 100000 - work = Work.create(work=long_text) - assert work.work == long_text - - -def test_work_with_special_characters() -> None: - with init_db(): - special_text = "Test with 🔥 emoji and\nnewlines\tand\ttabs" - work = Work.create(work=special_text) - assert work.work == special_text - - -def test_multiple_tags_per_work() -> None: - with init_db(): - work = Work.create(work="test work") - tags = [Tag.create(name=f"tag{i}") for i in range(10)] - - for tag in tags: - WorkTag.create(work=work, tag=tag) - - assert len(list(work.tags)) == 10 - - -def test_same_tag_multiple_works() -> None: - with init_db(): - tag = Tag.create(name="shared") - works = [Work.create(work=f"work{i}") for i in range(5)] - - for work in works: - WorkTag.create(work=work, tag=tag) - - assert len(list(tag.works)) == 5 diff --git a/tests/test_parser.py b/tests/test_parser.py index 3e9a903..b647940 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,7 +4,11 @@ import pytest from workedon.conf import settings -from workedon.exceptions import InvalidWorkError +from workedon.exceptions import ( + DateTimeInFutureError, + InvalidDateTimeError, + InvalidWorkError, +) from workedon.parser import InputParser from workedon.utils import now @@ -74,3 +78,348 @@ def test_parse_duration_returns_none_for_unknown_unit( r"\[\s*(\d+(?:\.\d+)?)\s*(sec)\s*\]", ) assert parser.parse_duration("[5 sec]") is None + + +# ---edge-cases--- + + +@pytest.fixture +def parser() -> InputParser: + return InputParser() + + +@pytest.mark.parametrize( + "input_str", + [ + "tomorrow at 5pm", + "next week", + "in 3 days", + "2099-12-31", + ], +) +def test_parse_datetime_future_raises_error(parser: InputParser, input_str: str) -> None: + with pytest.raises(DateTimeInFutureError): + parser.parse_datetime(input_str) + + +@pytest.mark.parametrize( + "input_str", + [ + "!@#$%^&*()", + "asdfghjkl", + "random gibberish", + "123abc456def", + ], +) +def test_parse_datetime_invalid_string_raises_error(parser: InputParser, input_str: str) -> None: + with pytest.raises(InvalidDateTimeError): + parser.parse_datetime(input_str) + + +def test_parse_datetime_whitespace_only_returns_now(parser: InputParser) -> None: + assert parser.parse_datetime(" ") == now() + assert parser.parse_datetime("\t\n") == now() + + +@pytest.mark.parametrize( + "input_str", + [ + "midnight", + "noon", + "3am", + "11:59pm", + "00:00", + "23:59", + ], +) +def test_parse_datetime_various_time_formats(parser: InputParser, input_str: str) -> None: + result = parser.parse_datetime(input_str) + assert result <= now() + + +def test_parse_datetime_edge_of_midnight(parser: InputParser) -> None: + with freeze_time("2024-01-15 00:01:00"): + try: + result = parser.parse_datetime("11:59pm") + except DateTimeInFutureError: + assert True + else: + assert result.hour == 23 + assert result.minute == 59 + assert result.date() == (now() - timedelta(days=1)).date() + + +@pytest.mark.parametrize( + "relative_time", + [ + "1 second ago", + "30 seconds ago", + "1 minute ago", + "59 minutes ago", + "1 hour ago", + "23 hours ago", + ], +) +def test_parse_datetime_relative_times(parser: InputParser, relative_time: str) -> None: + result = parser.parse_datetime(relative_time) + assert result <= now() + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("[0.5h]", 30), + ("[0.25hr]", 15), + ("[0.1hours]", 6), + ("[1000m]", 1000), + ("[0.01h]", 0.6), + ("[99999min]", 99999), + ], +) +def test_parse_duration_edge_values(parser: InputParser, input_str: str, expected: float) -> None: + assert parser.parse_duration(input_str) == expected + + +@pytest.mark.parametrize( + "input_str", + [ + "[]", + "[h]", + "[min]", + "[hours]", + "[minutes]", + "[0h]", # Valid but zero + ], +) +def test_parse_duration_no_numeric_value(parser: InputParser, input_str: str) -> None: + result = parser.parse_duration(input_str) + assert result is None or result == 0 + + +@pytest.mark.parametrize( + "input_str", + [ + "[1.2.3h]", + "[1..5m]", + "[.5.h]", + "[-5h]", + "[+3m]", + ], +) +def test_parse_duration_malformed_numbers(parser: InputParser, input_str: str) -> None: + assert parser.parse_duration(input_str) is None + + +@pytest.mark.parametrize( + "input_str", + [ + "[3x]", + "[5d]", + "[2s]", + "[10k]", + "[1.5days]", + ], +) +def test_parse_duration_invalid_units(parser: InputParser, input_str: str) -> None: + assert parser.parse_duration(input_str) is None + + +def test_parse_duration_multiple_brackets_uses_first(parser: InputParser) -> None: + assert parser.parse_duration("[30m] [60m] [90m]") == 30 + + +def test_parse_duration_case_insensitive(parser: InputParser) -> None: + assert parser.parse_duration("[2H]") == 120 + assert parser.parse_duration("[2Hr]") == 120 + assert parser.parse_duration("[2HRS]") == 120 + assert parser.parse_duration("[30MIN]") == 30 + assert parser.parse_duration("[30Minutes]") == 30 + + +def test_parse_duration_with_spaces(parser: InputParser) -> None: + assert parser.parse_duration("[ 2 h ]") == 120 + assert parser.parse_duration("[\t30\tm\t]") == 30 + + +def test_parse_duration_no_brackets(parser: InputParser) -> None: + assert parser.parse_duration("30m") is None + assert parser.parse_duration("2h") is None + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("#tag1 #tag2 #tag3", {"tag1", "tag2", "tag3"}), + ("#TAG #Tag #tag", {"TAG", "Tag", "tag"}), # Case preserved + ("#a #b #c #a #b", {"a", "b", "c"}), + ("#123 #456", {"123", "456"}), + ("#under_score #dash-tag", {"under_score", "dash-tag"}), + ("#mix123abc", {"mix123abc"}), + ], +) +def test_parse_tags_various_formats(parser: InputParser, input_str: str, expected: set) -> None: + assert parser.parse_tags(input_str) == expected + + +@pytest.mark.parametrize( + "input_str", + [ + "no tags here", + "has # space", + "##", + "###", + "#", + ], +) +def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: + assert parser.parse_tags(input_str) == set() + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("#tag!", {"tag"}), # Stops at special char + ("#tag@email", {"tag"}), + ("#tag.with.dots", {"tag"}), + ("#tag(parentheses)", {"tag"}), + ("#tag[brackets]", {"tag"}), + ("#tag{braces}", {"tag"}), + ], +) +def test_parse_tags_with_special_chars(parser: InputParser, input_str: str, expected: set) -> None: + assert parser.parse_tags(input_str) == expected + + +def test_parse_tags_ignores_non_word_symbols(parser: InputParser) -> None: + assert parser.parse_tags("#$ #! #?") == set() + + +def test_parse_tags_back_to_back(parser: InputParser) -> None: + assert parser.parse_tags("#one#two#three") == {"one", "two", "three"} + + +def test_parse_tags_empty_string(parser: InputParser) -> None: + assert parser.parse_tags("") == set() + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("work [30m] #tag", "work"), + ("#tag1 #tag2 work", "work"), + ("work #tag [60m]", "work"), + ("[2h] #dev work #qa [30m]", "work [30m]"), + (" multiple spaces ", "multiple spaces"), + ("\ttabs\tand\nnewlines\n", "tabs and newlines"), + ], +) +def test_clean_work_removes_tags_and_duration( + parser: InputParser, input_str: str, expected: str +) -> None: + assert parser.clean_work(input_str) == expected + + +def test_clean_work_preserves_special_chars(parser: InputParser) -> None: + assert parser.clean_work("work with @mentions") == "work with @mentions" + assert parser.clean_work("work & more stuff") == "work & more stuff" + assert parser.clean_work("work (in parens)") == "work (in parens)" + + +def test_clean_work_empty_after_cleaning(parser: InputParser) -> None: + assert parser.clean_work("#tag1 #tag2 [30m]") == "" + assert parser.clean_work(" [2h] ") == "" + + +def test_parse_work_without_separator(parser: InputParser) -> None: + work, dt, duration, tags = parser.parse("simple work") + assert work == "simple work" + assert dt == now() + assert duration is None + assert tags == set() + + +def test_parse_work_with_all_components(parser: InputParser) -> None: + work, dt, duration, tags = parser.parse("complex work [90m] #dev #qa @ yesterday") + assert work == "complex work" + assert duration == 90 + assert tags == {"dev", "qa"} + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_separator_in_work_text(parser: InputParser) -> None: + # Last @ should be the separator + work, dt, _duration, _tags = parser.parse("email to john@example.com @ yesterday") + assert work == "email to john@example.com" + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_multiple_separators(parser: InputParser) -> None: + # Should partition on the last @ + work, dt, _duration, _tags = parser.parse("work @ 3pm @ yesterday") + assert work == "work @ 3pm" + assert dt.date() == (now() - timedelta(days=1)).date() + + +def test_parse_empty_work_after_cleaning_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse("#tag [30m] @ yesterday") + + +def test_parse_separator_with_no_work_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse("@ yesterday") + + +def test_parse_whitespace_only_raises(parser: InputParser) -> None: + with pytest.raises(InvalidWorkError): + parser.parse(" ") + + +def test_parse_multiple_durations_uses_first(parser: InputParser) -> None: + _work, _dt, duration, _tags = parser.parse("work [30m] [60m] [90m]") + assert duration == 30 + + +def test_parse_preserves_work_punctuation(parser: InputParser) -> None: + work, _, _, _ = parser.parse("Work! With? Punctuation.") + assert work == "Work! With? Punctuation." + + +@pytest.mark.parametrize( + "input_str", + [ + "work @ tomorrow", + "work @ next week", + "work @ in 5 days", + ], +) +def test_parse_future_datetime_raises(parser: InputParser, input_str: str) -> None: + with pytest.raises(DateTimeInFutureError): + parser.parse(input_str) + + +def test_parse_invalid_datetime_raises(parser: InputParser) -> None: + with pytest.raises(InvalidDateTimeError): + parser.parse("work @ gibberish datetime") + + +def test_parse_extremely_long_work_text(parser: InputParser) -> None: + long_text = "a" * 10000 + work, _, _, _ = parser.parse(long_text) + assert work == long_text + + +def test_parse_many_tags(parser: InputParser) -> None: + tags_str = " ".join(f"#tag{i}" for i in range(100)) + _work, _, _, tags = parser.parse(f"work {tags_str}") + assert len(tags) == 100 + + +def test_parse_very_precise_duration(parser: InputParser) -> None: + _work, _, duration, _ = parser.parse("work [1.123456789h]") + assert duration == 67.41 # Rounded to 2 decimals + + +def test_parse_zero_duration(parser: InputParser) -> None: + _work, _, duration, _ = parser.parse("work [0h]") + assert duration == 0 diff --git a/tests/test_parser_edge_cases.py b/tests/test_parser_edge_cases.py deleted file mode 100644 index 918b522..0000000 --- a/tests/test_parser_edge_cases.py +++ /dev/null @@ -1,375 +0,0 @@ -"""Additional edge case tests for the InputParser.""" - -from datetime import timedelta - -from freezegun import freeze_time -import pytest - -from workedon.exceptions import ( - DateTimeInFutureError, - InvalidDateTimeError, - InvalidWorkError, -) -from workedon.parser import InputParser -from workedon.utils import now - - -@pytest.fixture -def parser() -> InputParser: - return InputParser() - - -# --- Edge cases for parse_datetime --- - - -@pytest.mark.parametrize( - "input_str", - [ - "tomorrow at 5pm", - "next week", - "in 3 days", - "2099-12-31", - ], -) -def test_parse_datetime_future_raises_error(parser: InputParser, input_str: str) -> None: - with pytest.raises(DateTimeInFutureError): - parser.parse_datetime(input_str) - - -@pytest.mark.parametrize( - "input_str", - [ - "!@#$%^&*()", - "asdfghjkl", - "random gibberish", - "123abc456def", - ], -) -def test_parse_datetime_invalid_string_raises_error(parser: InputParser, input_str: str) -> None: - with pytest.raises(InvalidDateTimeError): - parser.parse_datetime(input_str) - - -def test_parse_datetime_whitespace_only_returns_now(parser: InputParser) -> None: - assert parser.parse_datetime(" ") == now() - assert parser.parse_datetime("\t\n") == now() - - -@pytest.mark.parametrize( - "input_str", - [ - "midnight", - "noon", - "3am", - "11:59pm", - "00:00", - "23:59", - ], -) -def test_parse_datetime_various_time_formats(parser: InputParser, input_str: str) -> None: - result = parser.parse_datetime(input_str) - assert result <= now() - - -def test_parse_datetime_edge_of_midnight(parser: InputParser) -> None: - with freeze_time("2024-01-15 00:01:00"): - try: - result = parser.parse_datetime("11:59pm") - except DateTimeInFutureError: - assert True - else: - assert result.hour == 23 - assert result.minute == 59 - assert result.date() == (now() - timedelta(days=1)).date() - - -@pytest.mark.parametrize( - "relative_time", - [ - "1 second ago", - "30 seconds ago", - "1 minute ago", - "59 minutes ago", - "1 hour ago", - "23 hours ago", - ], -) -def test_parse_datetime_relative_times(parser: InputParser, relative_time: str) -> None: - result = parser.parse_datetime(relative_time) - assert result <= now() - - -# --- Edge cases for parse_duration --- - - -@pytest.mark.parametrize( - "input_str, expected", - [ - ("[0.5h]", 30), - ("[0.25hr]", 15), - ("[0.1hours]", 6), - ("[1000m]", 1000), - ("[0.01h]", 0.6), - ("[99999min]", 99999), - ], -) -def test_parse_duration_edge_values(parser: InputParser, input_str: str, expected: float) -> None: - assert parser.parse_duration(input_str) == expected - - -@pytest.mark.parametrize( - "input_str", - [ - "[]", - "[h]", - "[min]", - "[hours]", - "[minutes]", - "[0h]", # Valid but zero - ], -) -def test_parse_duration_no_numeric_value(parser: InputParser, input_str: str) -> None: - result = parser.parse_duration(input_str) - assert result is None or result == 0 - - -@pytest.mark.parametrize( - "input_str", - [ - "[1.2.3h]", - "[1..5m]", - "[.5.h]", - "[-5h]", - "[+3m]", - ], -) -def test_parse_duration_malformed_numbers(parser: InputParser, input_str: str) -> None: - assert parser.parse_duration(input_str) is None - - -@pytest.mark.parametrize( - "input_str", - [ - "[3x]", - "[5d]", - "[2s]", - "[10k]", - "[1.5days]", - ], -) -def test_parse_duration_invalid_units(parser: InputParser, input_str: str) -> None: - assert parser.parse_duration(input_str) is None - - -def test_parse_duration_multiple_brackets_uses_first(parser: InputParser) -> None: - assert parser.parse_duration("[30m] [60m] [90m]") == 30 - - -def test_parse_duration_case_insensitive(parser: InputParser) -> None: - assert parser.parse_duration("[2H]") == 120 - assert parser.parse_duration("[2Hr]") == 120 - assert parser.parse_duration("[2HRS]") == 120 - assert parser.parse_duration("[30MIN]") == 30 - assert parser.parse_duration("[30Minutes]") == 30 - - -def test_parse_duration_with_spaces(parser: InputParser) -> None: - assert parser.parse_duration("[ 2 h ]") == 120 - assert parser.parse_duration("[\t30\tm\t]") == 30 - - -def test_parse_duration_no_brackets(parser: InputParser) -> None: - assert parser.parse_duration("30m") is None - assert parser.parse_duration("2h") is None - - -# --- Edge cases for parse_tags --- - - -@pytest.mark.parametrize( - "input_str, expected", - [ - ("#tag1 #tag2 #tag3", {"tag1", "tag2", "tag3"}), - ("#TAG #Tag #tag", {"TAG", "Tag", "tag"}), # Case preserved - ("#a #b #c #a #b", {"a", "b", "c"}), - ("#123 #456", {"123", "456"}), - ("#under_score #dash-tag", {"under_score", "dash-tag"}), - ("#mix123abc", {"mix123abc"}), - ], -) -def test_parse_tags_various_formats(parser: InputParser, input_str: str, expected: set) -> None: - assert parser.parse_tags(input_str) == expected - - -@pytest.mark.parametrize( - "input_str", - [ - "no tags here", - "has # space", - "##", - "###", - "#", - ], -) -def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: - assert parser.parse_tags(input_str) == set() - - -@pytest.mark.parametrize( - "input_str, expected", - [ - ("#tag!", {"tag"}), # Stops at special char - ("#tag@email", {"tag"}), - ("#tag.with.dots", {"tag"}), - ("#tag(parentheses)", {"tag"}), - ("#tag[brackets]", {"tag"}), - ("#tag{braces}", {"tag"}), - ], -) -def test_parse_tags_with_special_chars(parser: InputParser, input_str: str, expected: set) -> None: - assert parser.parse_tags(input_str) == expected - - -def test_parse_tags_with_unicode(parser: InputParser) -> None: - # Emoji and other unicode should not match - assert parser.parse_tags("#🔥 #😀") == set() - - -def test_parse_tags_back_to_back(parser: InputParser) -> None: - assert parser.parse_tags("#one#two#three") == {"one", "two", "three"} - - -def test_parse_tags_empty_string(parser: InputParser) -> None: - assert parser.parse_tags("") == set() - - -# --- Edge cases for clean_work --- - - -@pytest.mark.parametrize( - "input_str, expected", - [ - ("work [30m] #tag", "work"), - ("#tag1 #tag2 work", "work"), - ("work #tag [60m]", "work"), - ("[2h] #dev work #qa [30m]", "work [30m]"), - (" multiple spaces ", "multiple spaces"), - ("\ttabs\tand\nnewlines\n", "tabs and newlines"), - ], -) -def test_clean_work_removes_tags_and_duration( - parser: InputParser, input_str: str, expected: str -) -> None: - assert parser.clean_work(input_str) == expected - - -def test_clean_work_preserves_special_chars(parser: InputParser) -> None: - assert parser.clean_work("work with @mentions") == "work with @mentions" - assert parser.clean_work("work & more stuff") == "work & more stuff" - assert parser.clean_work("work (in parens)") == "work (in parens)" - - -def test_clean_work_empty_after_cleaning(parser: InputParser) -> None: - assert parser.clean_work("#tag1 #tag2 [30m]") == "" - assert parser.clean_work(" [2h] ") == "" - - -# --- Edge cases for parse (main method) --- - - -def test_parse_work_without_separator(parser: InputParser) -> None: - work, dt, duration, tags = parser.parse("simple work") - assert work == "simple work" - assert dt == now() - assert duration is None - assert tags == set() - - -def test_parse_work_with_all_components(parser: InputParser) -> None: - work, dt, duration, tags = parser.parse("complex work [90m] #dev #qa @ yesterday") - assert work == "complex work" - assert duration == 90 - assert tags == {"dev", "qa"} - assert dt.date() == (now() - timedelta(days=1)).date() - - -def test_parse_separator_in_work_text(parser: InputParser) -> None: - # Last @ should be the separator - work, dt, _duration, _tags = parser.parse("email to john@example.com @ yesterday") - assert work == "email to john@example.com" - assert dt.date() == (now() - timedelta(days=1)).date() - - -def test_parse_multiple_separators(parser: InputParser) -> None: - # Should partition on the last @ - work, dt, _duration, _tags = parser.parse("work @ 3pm @ yesterday") - assert work == "work @ 3pm" - assert dt.date() == (now() - timedelta(days=1)).date() - - -def test_parse_empty_work_after_cleaning_raises(parser: InputParser) -> None: - with pytest.raises(InvalidWorkError): - parser.parse("#tag [30m] @ yesterday") - - -def test_parse_separator_with_no_work_raises(parser: InputParser) -> None: - with pytest.raises(InvalidWorkError): - parser.parse("@ yesterday") - - -def test_parse_whitespace_only_raises(parser: InputParser) -> None: - with pytest.raises(InvalidWorkError): - parser.parse(" ") - - -def test_parse_multiple_durations_uses_first(parser: InputParser) -> None: - _work, _dt, duration, _tags = parser.parse("work [30m] [60m] [90m]") - assert duration == 30 - - -def test_parse_preserves_work_punctuation(parser: InputParser) -> None: - work, _, _, _ = parser.parse("Work! With? Punctuation.") - assert work == "Work! With? Punctuation." - - -@pytest.mark.parametrize( - "input_str", - [ - "work @ tomorrow", - "work @ next week", - "work @ in 5 days", - ], -) -def test_parse_future_datetime_raises(parser: InputParser, input_str: str) -> None: - with pytest.raises(DateTimeInFutureError): - parser.parse(input_str) - - -def test_parse_invalid_datetime_raises(parser: InputParser) -> None: - with pytest.raises(InvalidDateTimeError): - parser.parse("work @ gibberish datetime") - - -# --- Boundary conditions --- - - -def test_parse_extremely_long_work_text(parser: InputParser) -> None: - long_text = "a" * 10000 - work, _, _, _ = parser.parse(long_text) - assert work == long_text - - -def test_parse_many_tags(parser: InputParser) -> None: - tags_str = " ".join(f"#tag{i}" for i in range(100)) - _work, _, _, tags = parser.parse(f"work {tags_str}") - assert len(tags) == 100 - - -def test_parse_very_precise_duration(parser: InputParser) -> None: - _work, _, duration, _ = parser.parse("work [1.123456789h]") - assert duration == 67.41 # Rounded to 2 decimals - - -def test_parse_zero_duration(parser: InputParser) -> None: - _work, _, duration, _ = parser.parse("work [0h]") - assert duration == 0 diff --git a/tests/test_utils.py b/tests/test_utils.py index fb96af5..5cd3aae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,9 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import cast import zoneinfo import click +from freezegun import freeze_time import pytest from workedon.conf import Settings, settings @@ -102,3 +103,43 @@ def handler() -> None: assert hasattr(handler, "__click_params__") assert {param.name for param in handler.__click_params__} == {"alpha", "beta"} + + +# ---edge-cases--- + + +def test_now_returns_timezone_aware() -> None: + settings.configure() + current = now() + assert current.tzinfo is not None + + +def test_to_internal_dt_converts_timezone() -> None: + settings.configure() + dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + internal = to_internal_dt(dt) + assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) + + +def test_to_internal_dt_preserves_date() -> None: + dt = datetime(2024, 6, 15, 23, 59, 59, tzinfo=timezone.utc) + internal = to_internal_dt(dt) + # Date might shift due to timezone conversion + assert abs((internal.date() - dt.date()).days) <= 1 + + +def test_now_frozen_time(monkeypatch: pytest.MonkeyPatch) -> None: + settings.configure() + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + with freeze_time("2024-01-15 12:00:00"): + current = now() + assert current.hour == 12 + assert current.minute == 0 + + +def test_to_internal_dt_with_naive_datetime_raises() -> None: + settings.configure() + dt = datetime(2024, 1, 1, 12, 0, 0) # No timezone + internal = to_internal_dt(dt) + assert internal.tzinfo is not None + assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) diff --git a/tests/test_utils_edge_cases.py b/tests/test_utils_edge_cases.py deleted file mode 100644 index 9d53a54..0000000 --- a/tests/test_utils_edge_cases.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Edge case tests for utility functions.""" - -from datetime import datetime, timezone -import zoneinfo - -from freezegun import freeze_time -import pytest - -from workedon.conf import settings -from workedon.utils import get_unique_hash, now, to_internal_dt - - -def test_get_unique_hash_returns_32_char_hex() -> None: - hash1 = get_unique_hash() - assert len(hash1) == 32 - assert all(c in "0123456789abcdef" for c in hash1) - - -def test_get_unique_hash_generates_unique_values() -> None: - hashes = [get_unique_hash() for _ in range(1000)] - assert len(set(hashes)) == 1000 - - -def test_now_returns_timezone_aware() -> None: - current = now() - assert current.tzinfo is not None - - -def test_now_uses_configured_timezone() -> None: - settings.configure() - current = now() - assert current.tzinfo == zoneinfo.ZoneInfo(settings.TIME_ZONE) - - -def test_to_internal_dt_removes_seconds() -> None: - dt = datetime(2024, 1, 1, 12, 30, 45, 999999, tzinfo=timezone.utc) - internal = to_internal_dt(dt) - assert internal.second == 0 - assert internal.microsecond == 0 - - -def test_to_internal_dt_converts_timezone() -> None: - settings.configure() - dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) - internal = to_internal_dt(dt) - assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) - - -def test_to_internal_dt_preserves_date() -> None: - dt = datetime(2024, 6, 15, 23, 59, 59, tzinfo=timezone.utc) - internal = to_internal_dt(dt) - # Date might shift due to timezone conversion - assert abs((internal.date() - dt.date()).days) <= 1 - - -def test_now_frozen_time(monkeypatch: pytest.MonkeyPatch) -> None: - settings.configure() - monkeypatch.setattr(settings, "TIME_ZONE", "UTC") - with freeze_time("2024-01-15 12:00:00"): - current = now() - assert current.hour == 12 - assert current.minute == 0 - - -def test_to_internal_dt_with_naive_datetime_raises() -> None: - settings.configure() - dt = datetime(2024, 1, 1, 12, 0, 0) # No timezone - internal = to_internal_dt(dt) - assert internal.tzinfo is not None - assert internal.tzinfo == zoneinfo.ZoneInfo(settings.internal_tz) From 313cd82dc16c0a4c839f5538a66c122deaa5e7da Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 20:03:50 -0500 Subject: [PATCH 21/33] Freeze every test at a fixed date --- tests/conftest.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 06f484a..9f85582 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,13 +20,10 @@ def runner() -> CliRunner: @pytest.fixture(autouse=True, scope="session") def freeze_clock_at_2359() -> Generator[None, None, None]: """ - Freeze every test at today's date but at 23:59:00, - so relative date parsing (“yesterday”, “tomorrow”, etc.) - is always based off of 11:59 PM local time. + Freeze every test at a fixed date (23:59:00) so relative date parsing + (“yesterday”, “tomorrow”, etc.) is deterministic across runs. """ - # capture now, then move to 23:59:00 of the same day - now = datetime.now() - target = now.replace(hour=23, minute=59, second=0, microsecond=0) + target = datetime(2024, 1, 10, 23, 59, 0) with freeze_time(target): yield From b8e656e543c19c52604b313b404bb73c8ddaae72 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Wed, 24 Dec 2025 20:51:24 -0500 Subject: [PATCH 22/33] fix flaky test_parse_datetime_edge_of_midnight with double freeze --- tests/test_parser.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index b647940..486255d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,4 +1,5 @@ -from datetime import timedelta +from datetime import datetime, timedelta +import zoneinfo from freezegun import freeze_time import pytest @@ -137,16 +138,25 @@ def test_parse_datetime_various_time_formats(parser: InputParser, input_str: str assert result <= now() -def test_parse_datetime_edge_of_midnight(parser: InputParser) -> None: - with freeze_time("2024-01-15 00:01:00"): - try: - result = parser.parse_datetime("11:59pm") - except DateTimeInFutureError: - assert True - else: - assert result.hour == 23 - assert result.minute == 59 - assert result.date() == (now() - timedelta(days=1)).date() +def test_parse_datetime_edge_of_midnight(monkeypatch: pytest.MonkeyPatch) -> None: + # We avoid `freeze_time` here because the session-wide freeze already sets a "now". + # In a prior version of this test, `InputParser` was created outside the inner + # freeze_time block via the `parser` fixture, while `now()` in the assertion used + # the inner freeze. That created two different "now" values and a flaky mismatch. + # By stubbing `workedon.parser.now` directly, both parsing and assertions share + # the same time source and the midnight edge case stays deterministic. + monkeypatch.setattr(settings, "TIME_ZONE", "UTC") + midnight_plus = datetime(2024, 1, 15, 0, 1, tzinfo=zoneinfo.ZoneInfo("UTC")) + monkeypatch.setattr("workedon.parser.now", lambda: midnight_plus) + parser = InputParser() + try: + result = parser.parse_datetime("11:59pm") + except DateTimeInFutureError: + assert True + else: + assert result.hour == 23 + assert result.minute == 59 + assert result.date() == (midnight_plus - timedelta(days=1)).date() @pytest.mark.parametrize( From a86846940f7c4d633e445c89aa1e8ccc6fb36177 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Sun, 28 Dec 2025 22:07:11 -0500 Subject: [PATCH 23/33] Update test_conf.py --- tests/test_conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_conf.py b/tests/test_conf.py index 2a991a2..97912b9 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -67,8 +67,9 @@ def test_configure_raises_on_exec_module_failure( monkeypatch.setattr(conf, "CONF_PATH", conf_path) settings = Settings() - with pytest.raises(CannotLoadSettingsError): + with pytest.raises(CannotLoadSettingsError) as excinfo: settings.configure() + assert "boom" in str(excinfo.value) def test_configure_raises_on_settings_file_creation_failure( From 028152cbb0d04b2c569466134119c8f4a130ab08 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Mon, 19 Jan 2026 14:47:20 -0500 Subject: [PATCH 24/33] fix debug check --- workedon/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workedon/cli.py b/workedon/cli.py index b5c336d..5156b0c 100644 --- a/workedon/cli.py +++ b/workedon/cli.py @@ -15,8 +15,8 @@ from .utils import add_options, load_settings from .workedon import fetch_tags, fetch_work, save_work -# Only ignore warnings if not in debug mode -if not os.environ.get("WORKEDON_DEBUG"): +# Only ignore warnings when the debug flag is set to "0" or not set +if os.environ.get("WORKEDON_DEBUG", "0") == "0": import warnings warnings.filterwarnings("ignore") From ad1c4888078fac29d661b20e2d6b561fd730a1a3 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Mon, 19 Jan 2026 16:02:39 -0500 Subject: [PATCH 25/33] refactor cli tests --- tests/test_cli.py | 30 +++++++++++++++++++ tests/test_cli_branches.py | 33 --------------------- uv.lock | 60 +++++++++++++++++++------------------- 3 files changed, 60 insertions(+), 63 deletions(-) delete mode 100644 tests/test_cli_branches.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 64b9398..36f93e6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,14 @@ from __future__ import annotations from datetime import datetime +import importlib import re +import warnings from click.testing import CliRunner, Result import pytest +import workedon from workedon import __version__, cli, exceptions from workedon.conf import CONF_PATH @@ -63,6 +66,33 @@ def test_main_with_subcommand_returns_early(runner: CliRunner) -> None: assert result.exit_code == 0 +def test_cli_import_skips_warning_filter_in_debug(monkeypatch: pytest.MonkeyPatch) -> None: + called = {"value": False} + + def record_call(*_args: object, **_kwargs: object) -> None: + called["value"] = True + + monkeypatch.setenv("WORKEDON_DEBUG", "1") + monkeypatch.setattr(warnings, "filterwarnings", record_call) + importlib.reload(cli) + importlib.reload(workedon) + + assert called["value"] is False + + +def test_cli_import_filters_warnings_without_debug(monkeypatch: pytest.MonkeyPatch) -> None: + called = {"value": False} + + def record_call(*_args: object, **_kwargs: object) -> None: + called["value"] = True + + monkeypatch.delenv("WORKEDON_DEBUG", raising=False) + monkeypatch.setattr(warnings, "filterwarnings", record_call) + importlib.reload(cli) + importlib.reload(workedon) + assert called["value"] is True + + # -- Basic save & fetch scenarios ------------------------------------------------ diff --git a/tests/test_cli_branches.py b/tests/test_cli_branches.py deleted file mode 100644 index 8151f8b..0000000 --- a/tests/test_cli_branches.py +++ /dev/null @@ -1,33 +0,0 @@ -import importlib - -import pytest - -import workedon -from workedon import cli - - -def test_cli_import_skips_warning_filter_in_debug(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("WORKEDON_DEBUG", "1") - importlib.reload(cli) - importlib.reload(workedon) - assert cli.CONTEXT_SETTINGS["help_option_names"] == ["-h", "--help"] - monkeypatch.delenv("WORKEDON_DEBUG", raising=False) - importlib.reload(cli) - importlib.reload(workedon) - - -def test_main_callback_runs_without_list_tags(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(cli.settings, "configure", lambda *args, **kwargs: None) - with cli.main.make_context("workedon", []) as ctx: - ctx.invoked_subcommand = None - with ctx: - cli.main.callback( - settings_path=False, - print_settings=False, - list_tags=False, - db_version=False, - sqlite_version=False, - print_db_path=False, - vacuum_db=False, - truncate_db=False, - ) diff --git a/uv.lock b/uv.lock index 7390b3c..d673c91 100644 --- a/uv.lock +++ b/uv.lock @@ -736,11 +736,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -2013,27 +2013,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/9d/59e955cc39206a0d58df5374808785c45ec2a8a2a230eb1638fbb4fe5c5d/ty-0.0.8.tar.gz", hash = "sha256:352ac93d6e0050763be57ad1e02087f454a842887e618ec14ac2103feac48676", size = 4828477, upload-time = "2025-12-29T13:50:07.193Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/2b/dd61f7e50a69c72f72c625d026e9ab64a0db62b2dd32e7426b520e2429c6/ty-0.0.8-py3-none-linux_armv6l.whl", hash = "sha256:a289d033c5576fa3b4a582b37d63395edf971cdbf70d2d2e6b8c95638d1a4fcd", size = 9853417, upload-time = "2025-12-29T13:50:08.979Z" }, - { url = "https://files.pythonhosted.org/packages/90/72/3f1d3c64a049a388e199de4493689a51fc6aa5ff9884c03dea52b4966657/ty-0.0.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:788ea97dc8153a94e476c4d57b2551a9458f79c187c4aba48fcb81f05372924a", size = 9657890, upload-time = "2025-12-29T13:50:27.867Z" }, - { url = "https://files.pythonhosted.org/packages/71/d1/08ac676bd536de3c2baba0deb60e67b3196683a2fabebfd35659d794b5e9/ty-0.0.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b5f1f3d3e230f35a29e520be7c3d90194a5229f755b721e9092879c00842d31", size = 9180129, upload-time = "2025-12-29T13:50:22.842Z" }, - { url = "https://files.pythonhosted.org/packages/af/93/610000e2cfeea1875900f73a375ba917624b0a008d4b8a6c18c894c8dbbc/ty-0.0.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6da9ed377fbbcec0a3b60b2ca5fd30496e15068f47cef2344ba87923e78ba996", size = 9683517, upload-time = "2025-12-29T13:50:18.658Z" }, - { url = "https://files.pythonhosted.org/packages/05/04/bef50ba7d8580b0140be597de5cc0ba9a63abe50d3f65560235f23658762/ty-0.0.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7d0a2bdce5e701d19eb8d46d9da0fe31340f079cecb7c438f5ac6897c73fc5ba", size = 9676279, upload-time = "2025-12-29T13:50:25.207Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b9/2aff1ef1f41b25898bc963173ae67fc8f04ca666ac9439a9c4e78d5cc0ff/ty-0.0.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef9078799d26d3cc65366e02392e2b78f64f72911b599e80a8497d2ec3117ddb", size = 10073015, upload-time = "2025-12-29T13:50:35.422Z" }, - { url = "https://files.pythonhosted.org/packages/df/0e/9feb6794b6ff0a157c3e6a8eb6365cbfa3adb9c0f7976e2abdc48615dd72/ty-0.0.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:54814ac39b4ab67cf111fc0a236818155cf49828976152378347a7678d30ee89", size = 10961649, upload-time = "2025-12-29T13:49:58.717Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3b/faf7328b14f00408f4f65c9d01efe52e11b9bcc4a79e06187b370457b004/ty-0.0.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4baf0a80398e8b6c68fa36ff85045a50ede1906cd4edb41fb4fab46d471f1d4", size = 10676190, upload-time = "2025-12-29T13:50:01.11Z" }, - { url = "https://files.pythonhosted.org/packages/64/a5/cfeca780de7eeab7852c911c06a84615a174d23e9ae08aae42a645771094/ty-0.0.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac8e23c3faefc579686799ef1649af8d158653169ad5c3a7df56b152781eeb67", size = 10438641, upload-time = "2025-12-29T13:50:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8d/8667c7e0ac9f13c461ded487c8d7350f440cd39ba866d0160a8e1b1efd6c/ty-0.0.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b558a647a073d0c25540aaa10f8947de826cb8757d034dd61ecf50ab8dbd77bf", size = 10214082, upload-time = "2025-12-29T13:50:31.531Z" }, - { url = "https://files.pythonhosted.org/packages/f8/11/e563229870e2c1d089e7e715c6c3b7605a34436dddf6f58e9205823020c2/ty-0.0.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8c0104327bf480508bd81f320e22074477df159d9eff85207df39e9c62ad5e96", size = 9664364, upload-time = "2025-12-29T13:50:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/05b79b778bf5237bcd7ee08763b226130aa8da872cbb151c8cfa2e886203/ty-0.0.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:496f1cb87261dd1a036a5609da80ee13de2e6ee4718a661bfa2afb91352fe528", size = 9679440, upload-time = "2025-12-29T13:50:11.289Z" }, - { url = "https://files.pythonhosted.org/packages/12/b5/23ba887769c4a7b8abfd1b6395947dc3dcc87533fbf86379d3a57f87ae8f/ty-0.0.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c488031f92a075ae39d13ac6295fdce2141164ec38c5d47aa8dc24ee3afa37e", size = 9808201, upload-time = "2025-12-29T13:50:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/f8/90/5a82ac0a0707db55376922aed80cd5fca6b2e6d6e9bcd8c286e6b43b4084/ty-0.0.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90d6f08c5982fa3e802b8918a32e326153519077b827f91c66eea4913a86756a", size = 10313262, upload-time = "2025-12-29T13:50:03.306Z" }, - { url = "https://files.pythonhosted.org/packages/14/f7/ff97f37f0a75db9495ddbc47738ec4339837867c4bfa145bdcfbd0d1eb2f/ty-0.0.8-py3-none-win32.whl", hash = "sha256:d7f460ad6fc9325e9cc8ea898949bbd88141b4609d1088d7ede02ce2ef06e776", size = 9254675, upload-time = "2025-12-29T13:50:33.35Z" }, - { url = "https://files.pythonhosted.org/packages/af/51/eba5d83015e04630002209e3590c310a0ff1d26e1815af204a322617a42e/ty-0.0.8-py3-none-win_amd64.whl", hash = "sha256:1641fb8dedc3d2da43279d21c3c7c1f80d84eae5c264a1e8daa544458e433c19", size = 10131382, upload-time = "2025-12-29T13:50:13.719Z" }, - { url = "https://files.pythonhosted.org/packages/38/1c/0d8454ff0f0f258737ecfe84f6e508729191d29663b404832f98fa5626b7/ty-0.0.8-py3-none-win_arm64.whl", hash = "sha256:ec74f022f315bede478ecae1277a01ab618e6500c1d68450d7883f5cd6ed554a", size = 9636374, upload-time = "2025-12-29T13:50:16.344Z" }, +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, + { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, + { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, + { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, ] [[package]] @@ -2068,11 +2068,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -2115,7 +2115,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2123,9 +2123,9 @@ dependencies = [ { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] From 5f47d7b6db4bcd2e0d1f8e9587b7a938094cc7a4 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Thu, 22 Jan 2026 19:28:20 -0500 Subject: [PATCH 26/33] Update test_utils.py --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5cd3aae..a38c5a5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -137,7 +137,7 @@ def test_now_frozen_time(monkeypatch: pytest.MonkeyPatch) -> None: assert current.minute == 0 -def test_to_internal_dt_with_naive_datetime_raises() -> None: +def test_to_internal_dt_sets_internal_timezone_for_naive_datetime() -> None: settings.configure() dt = datetime(2024, 1, 1, 12, 0, 0) # No timezone internal = to_internal_dt(dt) From 9dd82e15f03f36fcfff65713d8bb55744a554fdc Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Thu, 22 Jan 2026 19:32:43 -0500 Subject: [PATCH 27/33] add check constraint for empty tags --- tests/test_models.py | 8 +++----- workedon/models.py | 7 ++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 88969ac..d88ae15 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -261,11 +261,9 @@ def test_tag_requires_unique_name() -> None: Tag.create(name="unique") -def test_tag_allows_empty_string_name() -> None: - # This might be undesirable but tests current behavior - with init_db(): - tag = Tag.create(name="") - assert tag.name == "" +def test_tag_rejects_empty_string_name() -> None: + with init_db(), pytest.raises(IntegrityError): + Tag.create(name="") def test_work_tag_cascade_delete() -> None: diff --git a/workedon/models.py b/workedon/models.py index 152925d..1b52752 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -7,6 +7,7 @@ import click from peewee import ( CharField, + Check, CompositeKey, DateTimeField, FloatField, @@ -121,7 +122,11 @@ class Tag(Model): """ uuid: CharField = CharField(primary_key=True, null=False, default=get_unique_hash) - name: CharField = CharField(unique=True, null=False) + name: CharField = CharField( + unique=True, + null=False, + constraints=[Check("name != ''")], + ) created: DateTimeField = DateTimeField( null=False, formats=[settings.internal_dt_format], default=get_default_time ) From ab1b58c61e35dfca54cce39c9a924ebeeca098be Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 3 Feb 2026 22:27:07 -0500 Subject: [PATCH 28/33] normalize tags --- workedon/models.py | 2 -- workedon/workedon.py | 12 ++++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/workedon/models.py b/workedon/models.py index 1b52752..8436bea 100644 --- a/workedon/models.py +++ b/workedon/models.py @@ -7,7 +7,6 @@ import click from peewee import ( CharField, - Check, CompositeKey, DateTimeField, FloatField, @@ -125,7 +124,6 @@ class Tag(Model): name: CharField = CharField( unique=True, null=False, - constraints=[Check("name != ''")], ) created: DateTimeField = DateTimeField( null=False, formats=[settings.internal_dt_format], default=get_default_time diff --git a/workedon/workedon.py b/workedon/workedon.py index a1e528e..a18e0d5 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Iterator +from collections.abc import Iterable, Iterator import datetime import operator as op import re @@ -23,6 +23,14 @@ from .utils import now, to_internal_dt +def _normalize_tags(tags: Iterable[str]) -> set[str]: + """ + Normalize tags to lowercase and drop empty/whitespace-only values. + """ + normalized = {tag.strip().lower() for tag in tags} + return {tag for tag in normalized if tag} + + def save_work(work: tuple[str, ...], tags_opt: tuple[str, ...], duration_opt: str) -> None: """ Save work from user input @@ -32,7 +40,7 @@ def save_work(work: tuple[str, ...], tags_opt: tuple[str, ...], duration_opt: st work_text, dt, duration, tags = parser.parse(work_desc) if tags_opt: tags.update(set(tags_opt)) - tags = {tag.lower() for tag in tags} + tags = _normalize_tags(tags) if duration_opt: minutes = parser.parse_duration(f"[{duration_opt.strip()}]") From abeff07d3fb0e204de64448d99852e45289530ab Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 3 Feb 2026 22:27:56 -0500 Subject: [PATCH 29/33] update tag tests --- tests/test_models.py | 5 ----- tests/test_workedon.py | 8 ++++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index d88ae15..b0f62d1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -261,11 +261,6 @@ def test_tag_requires_unique_name() -> None: Tag.create(name="unique") -def test_tag_rejects_empty_string_name() -> None: - with init_db(), pytest.raises(IntegrityError): - Tag.create(name="") - - def test_work_tag_cascade_delete() -> None: with init_db(): work = Work.create(work="test work") diff --git a/tests/test_workedon.py b/tests/test_workedon.py index 132211d..39672df 100644 --- a/tests/test_workedon.py +++ b/tests/test_workedon.py @@ -130,6 +130,14 @@ def test_save_work_creates_tags_from_option() -> None: assert WorkTag.select().count() == 1 +def test_save_work_ignores_empty_tag_option() -> None: + workedon.save_work(("build", "feature"), ("", " ", "dev"), "") + with init_db(): + assert Tag.select().where(Tag.name == "dev").exists() + assert Tag.select().where(Tag.name == "").count() == 0 + assert WorkTag.select().count() == 1 + + def test_save_work_raises_cannot_save_on_failure( monkeypatch: pytest.MonkeyPatch, ) -> None: From 4bc8372c2bed88f1bd7543fe6ad01e3c63e31847 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 3 Feb 2026 22:31:31 -0500 Subject: [PATCH 30/33] Reduce redundant CLI/parser test cases --- tests/test_cli.py | 44 +------------------------------------------- tests/test_parser.py | 35 ----------------------------------- 2 files changed, 1 insertion(+), 78 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 36f93e6..afdb23c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,8 +30,6 @@ def save_and_verify(runner: CliRunner, command: str, description: str) -> None: "options", [ ["-h"], - ["--help"], - ["what", "-h"], ["what", "--help"], ], ) @@ -45,7 +43,6 @@ def test_help(runner: CliRunner, options: list[str]) -> None: @pytest.mark.parametrize( "options", [ - ["-v"], ["--version"], ], ) @@ -100,8 +97,6 @@ def record_call(*_args: object, **_kwargs: object) -> None: "command, description", [ ("washing the car", "washing the car"), - ("studying for the SAT @ 3pm friday", "studying for the SAT"), - ("pissing my wife off @ 2:30pm yesterday", "pissing my wife off"), ("writing tests @ 9 hours ago", "writing tests"), ], ) @@ -120,7 +115,6 @@ def test_save_and_fetch(runner: CliRunner, command: str, description: str) -> No "command, description, valid", [ ("building workedon", "building workedon", True), - ("studying for the GRE", "studying for the GRE", True), ("talking to my brother @ 3pm 3 years ago", "talking to my brother", False), ], ) @@ -224,7 +218,6 @@ def test_timezone_option( "command, flag", [ ("calling 911", ["--count", "1"]), # 18 - ("weights at the gym", ["--count", "1"]), # 19 ("yard work at home @ 3pm friday", ["--on", "friday"]), # 20 ("learning guitar @ 9pm friday", ["--at", "9pm friday"]), # 21 ( @@ -232,24 +225,15 @@ def test_timezone_option( ["--since", "August 1 1947", "-r", "-n", "1"], ), # 22 ("framing a photo @ 1:34pm yesterday", ["--yesterday"]), # 23 - ("taking pictures @ 12:34pm yesterday", ["-e"]), # 24 ("training for a 4k", ["--today"]), # 25 - ("training for a 10k", ["-o"]), # 26 ("setting up my homelab @ 1 hour ago", ["--past-day"]), # 27 - ("setting up my garden @ 2 hours ago", ["-d"]), # 28 ("setting up my garage @ 2pm 6 days ago", ["--past-week", "-r", "-n", "1"]), # 29 - ("setting up my kitchen @ 1pm 6 days ago", ["-w", "-r", "-n", "1"]), # 30 ("cleaning my car @ 2pm 27 days ago", ["--past-month", "-r", "-n", "1"]), # 31 - ("vacuuming my car @ 1pm 27 days ago", ["-m", "-r", "-n", "1"]), # 32 ("learning to make sushi @ 2pm 360 days ago", ["--past-year", "-r", "-n", "1"]), # 33 - ("learning to brew soy sauce @ 1pm 360 days ago", ["-y", "-r", "-n", "1"]), # 34 ( "learning to drive @ 3pm June 3rd 2020", ["--from", "June 2nd 2020", "--to", "June 4th 2020"], ), # 35 - ("learning to cook @ 3pm yesterday", ["-f", "2 days ago", "-t", "3:05pm yesterday"]), # 36 - ("watching tv @ 9am", ["-g"]), # 37 - ("taking wife shopping @ 3pm", ["--no-page"]), # 38 ], ) def test_save_and_fetch_others(runner: CliRunner, command: str, flag: list[str]) -> None: @@ -296,7 +280,7 @@ def test_delete_empty(runner: CliRunner) -> None: # -- Text-only output ----------------------------------------------------------- -@pytest.mark.parametrize("flag", [["-l"], ["--text-only"]]) +@pytest.mark.parametrize("flag", [["--text-only"]]) def test_text_only(runner: CliRunner, flag: list[str]) -> None: description = "vacuuming" save_and_verify(runner, f"{description} @ 2am", description) @@ -448,11 +432,8 @@ def test_fetch_errors(runner: CliRunner, flags: list[str], detail: str) -> None: "input_text, expected_tags", [ ("coding new feature #dev", {"dev"}), - ("daily standup #work #team", {"work", "team"}), ("writing unit tests #TDD #QA", {"tdd", "qa"}), - ("prepping lunch #Home #MealPrep", {"home", "mealprep"}), ("empty tag test #", set()), # should be ignored - ("duplicate tags test #fun #fun #fun", {"fun"}), ], ) def test_cli_tag_parsing_and_display( @@ -473,17 +454,9 @@ def test_cli_tag_parsing_and_display( "input_text, expected_tags", [ ("tag with dash #in-progress", {"in-progress"}), - ("tag with underscore #code_review", {"code_review"}), - ("numeric tag #123", {"123"}), - ("alphanumeric mix #r2d2", {"r2d2"}), ("non-word chars #cool! #$money", {"cool"}), # `$money` ignored ("emoji tag #🔥", set()), # emoji-only tag ignored - ("weird spacing # spaced", {"spaced"}), # space after `#` ignored ("back-to-back #one#two#three", {"one", "two", "three"}), # should catch all - ("URL in tag context #http123", {"http123"}), # safe string - ("mixed case tags #Dev #DEV #dev", {"dev"}), # normalized - ("invalid format tags ##double", {"double"}), # invalid - ("edge #", set()), # empty tag ], ) def test_weird_tag_parsing(runner: CliRunner, input_text: str, expected_tags: set[str]) -> None: @@ -505,11 +478,7 @@ def test_weird_tag_parsing(runner: CliRunner, input_text: str, expected_tags: se "save_cmd, tag_flag, expect_match", [ ("working on #devtools", ["--tag", "devtools"], True), - ("#in-progress cleanup", ["--tag", "in-progress"], True), ("writing code #DEV", ["--tag", "dev"], True), # case-insensitive - ("refactoring #code_review", ["--tag", "code_review"], True), - ("invalid ##doubletag", ["--tag", "doubletag"], True), - ("emoji #🔥", ["--tag", "🔥"], False), ("no tags here", ["--tag", "nothing"], False), ], ) @@ -544,10 +513,7 @@ def test_list_tags_outputs_saved_tags(runner: CliRunner) -> None: [ ("doing taxes [1h]", "Duration: 1.0 hr", ["--duration-unit", "hr"]), ("filing returns [1.5hr]", "Duration: 90.0 min", ["--duration-unit", "min"]), - ("gym workout [90min]", "Duration: 1.5 hr", ["--duration-unit", "hr"]), - ("long call [2.25hours]", "Duration: 2.25 hr", ["--duration-unit", "hr"]), ("fast errand [45m] [90m]", "Duration: 0.75 hr", ["--duration-unit", "hr"]), - ("short nap [15MINS]", "Duration: 15.0 min", []), ], ) def test_cli_duration_parsing_and_display( @@ -563,11 +529,8 @@ def test_cli_duration_parsing_and_display( "input_text", [ "broken input [3x]", - "missing unit [123]", - "non-numeric [abcmin]", "[1.2.3h]", "empty brackets []", - "only unit [hrs]", ], ) def test_cli_malformed_durations_are_ignored(runner: CliRunner, input_text: str) -> None: @@ -586,10 +549,7 @@ def test_cli_malformed_durations_are_ignored(runner: CliRunner, input_text: str) ("task1 [2h] @ 1pm", ["--duration", "=120m"], True), ("task2 [90m] @ 2pm", ["--duration", "<2h"], True), ("task3 [3h] @ 3pm", ["--duration", ">=3h"], True), - ("task4 [60m] @ 4pm", ["--duration", "<=1h"], True), - ("task5 [1.5h] @ 5pm", ["--duration", "=90m"], True), ("task6 [45m] @ 6pm", ["--duration", ">1h"], False), - ("task7 [2h] @ 7pm", ["--duration", "<2h"], False), ], ) def test_cli_duration_filter( @@ -609,8 +569,6 @@ def test_cli_duration_filter( "invalid_filter_flag", [ ["--duration", "=>3h"], - ["--duration", "3hors"], - ["--duration", "3h<"], ["--duration", "3x"], ], ) diff --git a/tests/test_parser.py b/tests/test_parser.py index 486255d..c1f8aa7 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -93,8 +93,6 @@ def parser() -> InputParser: "input_str", [ "tomorrow at 5pm", - "next week", - "in 3 days", "2099-12-31", ], ) @@ -108,8 +106,6 @@ def test_parse_datetime_future_raises_error(parser: InputParser, input_str: str) [ "!@#$%^&*()", "asdfghjkl", - "random gibberish", - "123abc456def", ], ) def test_parse_datetime_invalid_string_raises_error(parser: InputParser, input_str: str) -> None: @@ -126,10 +122,7 @@ def test_parse_datetime_whitespace_only_returns_now(parser: InputParser) -> None "input_str", [ "midnight", - "noon", "3am", - "11:59pm", - "00:00", "23:59", ], ) @@ -162,10 +155,7 @@ def test_parse_datetime_edge_of_midnight(monkeypatch: pytest.MonkeyPatch) -> Non @pytest.mark.parametrize( "relative_time", [ - "1 second ago", - "30 seconds ago", "1 minute ago", - "59 minutes ago", "1 hour ago", "23 hours ago", ], @@ -179,10 +169,7 @@ def test_parse_datetime_relative_times(parser: InputParser, relative_time: str) "input_str, expected", [ ("[0.5h]", 30), - ("[0.25hr]", 15), ("[0.1hours]", 6), - ("[1000m]", 1000), - ("[0.01h]", 0.6), ("[99999min]", 99999), ], ) @@ -194,10 +181,7 @@ def test_parse_duration_edge_values(parser: InputParser, input_str: str, expecte "input_str", [ "[]", - "[h]", "[min]", - "[hours]", - "[minutes]", "[0h]", # Valid but zero ], ) @@ -210,8 +194,6 @@ def test_parse_duration_no_numeric_value(parser: InputParser, input_str: str) -> "input_str", [ "[1.2.3h]", - "[1..5m]", - "[.5.h]", "[-5h]", "[+3m]", ], @@ -225,8 +207,6 @@ def test_parse_duration_malformed_numbers(parser: InputParser, input_str: str) - [ "[3x]", "[5d]", - "[2s]", - "[10k]", "[1.5days]", ], ) @@ -241,14 +221,11 @@ def test_parse_duration_multiple_brackets_uses_first(parser: InputParser) -> Non def test_parse_duration_case_insensitive(parser: InputParser) -> None: assert parser.parse_duration("[2H]") == 120 assert parser.parse_duration("[2Hr]") == 120 - assert parser.parse_duration("[2HRS]") == 120 assert parser.parse_duration("[30MIN]") == 30 - assert parser.parse_duration("[30Minutes]") == 30 def test_parse_duration_with_spaces(parser: InputParser) -> None: assert parser.parse_duration("[ 2 h ]") == 120 - assert parser.parse_duration("[\t30\tm\t]") == 30 def test_parse_duration_no_brackets(parser: InputParser) -> None: @@ -261,10 +238,7 @@ def test_parse_duration_no_brackets(parser: InputParser) -> None: [ ("#tag1 #tag2 #tag3", {"tag1", "tag2", "tag3"}), ("#TAG #Tag #tag", {"TAG", "Tag", "tag"}), # Case preserved - ("#a #b #c #a #b", {"a", "b", "c"}), - ("#123 #456", {"123", "456"}), ("#under_score #dash-tag", {"under_score", "dash-tag"}), - ("#mix123abc", {"mix123abc"}), ], ) def test_parse_tags_various_formats(parser: InputParser, input_str: str, expected: set) -> None: @@ -275,9 +249,7 @@ def test_parse_tags_various_formats(parser: InputParser, input_str: str, expecte "input_str", [ "no tags here", - "has # space", "##", - "###", "#", ], ) @@ -289,11 +261,7 @@ def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: "input_str, expected", [ ("#tag!", {"tag"}), # Stops at special char - ("#tag@email", {"tag"}), - ("#tag.with.dots", {"tag"}), ("#tag(parentheses)", {"tag"}), - ("#tag[brackets]", {"tag"}), - ("#tag{braces}", {"tag"}), ], ) def test_parse_tags_with_special_chars(parser: InputParser, input_str: str, expected: set) -> None: @@ -316,10 +284,7 @@ def test_parse_tags_empty_string(parser: InputParser) -> None: "input_str, expected", [ ("work [30m] #tag", "work"), - ("#tag1 #tag2 work", "work"), - ("work #tag [60m]", "work"), ("[2h] #dev work #qa [30m]", "work [30m]"), - (" multiple spaces ", "multiple spaces"), ("\ttabs\tand\nnewlines\n", "tabs and newlines"), ], ) From bee2fc8569025583876ec826edbba2de04b1b151 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 3 Feb 2026 22:38:38 -0500 Subject: [PATCH 31/33] Revert "Reduce redundant CLI/parser test cases" This reverts commit 4bc8372c2bed88f1bd7543fe6ad01e3c63e31847. --- tests/test_cli.py | 44 +++++++++++++++++++++++++++++++++++++++++++- tests/test_parser.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index afdb23c..36f93e6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,6 +30,8 @@ def save_and_verify(runner: CliRunner, command: str, description: str) -> None: "options", [ ["-h"], + ["--help"], + ["what", "-h"], ["what", "--help"], ], ) @@ -43,6 +45,7 @@ def test_help(runner: CliRunner, options: list[str]) -> None: @pytest.mark.parametrize( "options", [ + ["-v"], ["--version"], ], ) @@ -97,6 +100,8 @@ def record_call(*_args: object, **_kwargs: object) -> None: "command, description", [ ("washing the car", "washing the car"), + ("studying for the SAT @ 3pm friday", "studying for the SAT"), + ("pissing my wife off @ 2:30pm yesterday", "pissing my wife off"), ("writing tests @ 9 hours ago", "writing tests"), ], ) @@ -115,6 +120,7 @@ def test_save_and_fetch(runner: CliRunner, command: str, description: str) -> No "command, description, valid", [ ("building workedon", "building workedon", True), + ("studying for the GRE", "studying for the GRE", True), ("talking to my brother @ 3pm 3 years ago", "talking to my brother", False), ], ) @@ -218,6 +224,7 @@ def test_timezone_option( "command, flag", [ ("calling 911", ["--count", "1"]), # 18 + ("weights at the gym", ["--count", "1"]), # 19 ("yard work at home @ 3pm friday", ["--on", "friday"]), # 20 ("learning guitar @ 9pm friday", ["--at", "9pm friday"]), # 21 ( @@ -225,15 +232,24 @@ def test_timezone_option( ["--since", "August 1 1947", "-r", "-n", "1"], ), # 22 ("framing a photo @ 1:34pm yesterday", ["--yesterday"]), # 23 + ("taking pictures @ 12:34pm yesterday", ["-e"]), # 24 ("training for a 4k", ["--today"]), # 25 + ("training for a 10k", ["-o"]), # 26 ("setting up my homelab @ 1 hour ago", ["--past-day"]), # 27 + ("setting up my garden @ 2 hours ago", ["-d"]), # 28 ("setting up my garage @ 2pm 6 days ago", ["--past-week", "-r", "-n", "1"]), # 29 + ("setting up my kitchen @ 1pm 6 days ago", ["-w", "-r", "-n", "1"]), # 30 ("cleaning my car @ 2pm 27 days ago", ["--past-month", "-r", "-n", "1"]), # 31 + ("vacuuming my car @ 1pm 27 days ago", ["-m", "-r", "-n", "1"]), # 32 ("learning to make sushi @ 2pm 360 days ago", ["--past-year", "-r", "-n", "1"]), # 33 + ("learning to brew soy sauce @ 1pm 360 days ago", ["-y", "-r", "-n", "1"]), # 34 ( "learning to drive @ 3pm June 3rd 2020", ["--from", "June 2nd 2020", "--to", "June 4th 2020"], ), # 35 + ("learning to cook @ 3pm yesterday", ["-f", "2 days ago", "-t", "3:05pm yesterday"]), # 36 + ("watching tv @ 9am", ["-g"]), # 37 + ("taking wife shopping @ 3pm", ["--no-page"]), # 38 ], ) def test_save_and_fetch_others(runner: CliRunner, command: str, flag: list[str]) -> None: @@ -280,7 +296,7 @@ def test_delete_empty(runner: CliRunner) -> None: # -- Text-only output ----------------------------------------------------------- -@pytest.mark.parametrize("flag", [["--text-only"]]) +@pytest.mark.parametrize("flag", [["-l"], ["--text-only"]]) def test_text_only(runner: CliRunner, flag: list[str]) -> None: description = "vacuuming" save_and_verify(runner, f"{description} @ 2am", description) @@ -432,8 +448,11 @@ def test_fetch_errors(runner: CliRunner, flags: list[str], detail: str) -> None: "input_text, expected_tags", [ ("coding new feature #dev", {"dev"}), + ("daily standup #work #team", {"work", "team"}), ("writing unit tests #TDD #QA", {"tdd", "qa"}), + ("prepping lunch #Home #MealPrep", {"home", "mealprep"}), ("empty tag test #", set()), # should be ignored + ("duplicate tags test #fun #fun #fun", {"fun"}), ], ) def test_cli_tag_parsing_and_display( @@ -454,9 +473,17 @@ def test_cli_tag_parsing_and_display( "input_text, expected_tags", [ ("tag with dash #in-progress", {"in-progress"}), + ("tag with underscore #code_review", {"code_review"}), + ("numeric tag #123", {"123"}), + ("alphanumeric mix #r2d2", {"r2d2"}), ("non-word chars #cool! #$money", {"cool"}), # `$money` ignored ("emoji tag #🔥", set()), # emoji-only tag ignored + ("weird spacing # spaced", {"spaced"}), # space after `#` ignored ("back-to-back #one#two#three", {"one", "two", "three"}), # should catch all + ("URL in tag context #http123", {"http123"}), # safe string + ("mixed case tags #Dev #DEV #dev", {"dev"}), # normalized + ("invalid format tags ##double", {"double"}), # invalid + ("edge #", set()), # empty tag ], ) def test_weird_tag_parsing(runner: CliRunner, input_text: str, expected_tags: set[str]) -> None: @@ -478,7 +505,11 @@ def test_weird_tag_parsing(runner: CliRunner, input_text: str, expected_tags: se "save_cmd, tag_flag, expect_match", [ ("working on #devtools", ["--tag", "devtools"], True), + ("#in-progress cleanup", ["--tag", "in-progress"], True), ("writing code #DEV", ["--tag", "dev"], True), # case-insensitive + ("refactoring #code_review", ["--tag", "code_review"], True), + ("invalid ##doubletag", ["--tag", "doubletag"], True), + ("emoji #🔥", ["--tag", "🔥"], False), ("no tags here", ["--tag", "nothing"], False), ], ) @@ -513,7 +544,10 @@ def test_list_tags_outputs_saved_tags(runner: CliRunner) -> None: [ ("doing taxes [1h]", "Duration: 1.0 hr", ["--duration-unit", "hr"]), ("filing returns [1.5hr]", "Duration: 90.0 min", ["--duration-unit", "min"]), + ("gym workout [90min]", "Duration: 1.5 hr", ["--duration-unit", "hr"]), + ("long call [2.25hours]", "Duration: 2.25 hr", ["--duration-unit", "hr"]), ("fast errand [45m] [90m]", "Duration: 0.75 hr", ["--duration-unit", "hr"]), + ("short nap [15MINS]", "Duration: 15.0 min", []), ], ) def test_cli_duration_parsing_and_display( @@ -529,8 +563,11 @@ def test_cli_duration_parsing_and_display( "input_text", [ "broken input [3x]", + "missing unit [123]", + "non-numeric [abcmin]", "[1.2.3h]", "empty brackets []", + "only unit [hrs]", ], ) def test_cli_malformed_durations_are_ignored(runner: CliRunner, input_text: str) -> None: @@ -549,7 +586,10 @@ def test_cli_malformed_durations_are_ignored(runner: CliRunner, input_text: str) ("task1 [2h] @ 1pm", ["--duration", "=120m"], True), ("task2 [90m] @ 2pm", ["--duration", "<2h"], True), ("task3 [3h] @ 3pm", ["--duration", ">=3h"], True), + ("task4 [60m] @ 4pm", ["--duration", "<=1h"], True), + ("task5 [1.5h] @ 5pm", ["--duration", "=90m"], True), ("task6 [45m] @ 6pm", ["--duration", ">1h"], False), + ("task7 [2h] @ 7pm", ["--duration", "<2h"], False), ], ) def test_cli_duration_filter( @@ -569,6 +609,8 @@ def test_cli_duration_filter( "invalid_filter_flag", [ ["--duration", "=>3h"], + ["--duration", "3hors"], + ["--duration", "3h<"], ["--duration", "3x"], ], ) diff --git a/tests/test_parser.py b/tests/test_parser.py index c1f8aa7..486255d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -93,6 +93,8 @@ def parser() -> InputParser: "input_str", [ "tomorrow at 5pm", + "next week", + "in 3 days", "2099-12-31", ], ) @@ -106,6 +108,8 @@ def test_parse_datetime_future_raises_error(parser: InputParser, input_str: str) [ "!@#$%^&*()", "asdfghjkl", + "random gibberish", + "123abc456def", ], ) def test_parse_datetime_invalid_string_raises_error(parser: InputParser, input_str: str) -> None: @@ -122,7 +126,10 @@ def test_parse_datetime_whitespace_only_returns_now(parser: InputParser) -> None "input_str", [ "midnight", + "noon", "3am", + "11:59pm", + "00:00", "23:59", ], ) @@ -155,7 +162,10 @@ def test_parse_datetime_edge_of_midnight(monkeypatch: pytest.MonkeyPatch) -> Non @pytest.mark.parametrize( "relative_time", [ + "1 second ago", + "30 seconds ago", "1 minute ago", + "59 minutes ago", "1 hour ago", "23 hours ago", ], @@ -169,7 +179,10 @@ def test_parse_datetime_relative_times(parser: InputParser, relative_time: str) "input_str, expected", [ ("[0.5h]", 30), + ("[0.25hr]", 15), ("[0.1hours]", 6), + ("[1000m]", 1000), + ("[0.01h]", 0.6), ("[99999min]", 99999), ], ) @@ -181,7 +194,10 @@ def test_parse_duration_edge_values(parser: InputParser, input_str: str, expecte "input_str", [ "[]", + "[h]", "[min]", + "[hours]", + "[minutes]", "[0h]", # Valid but zero ], ) @@ -194,6 +210,8 @@ def test_parse_duration_no_numeric_value(parser: InputParser, input_str: str) -> "input_str", [ "[1.2.3h]", + "[1..5m]", + "[.5.h]", "[-5h]", "[+3m]", ], @@ -207,6 +225,8 @@ def test_parse_duration_malformed_numbers(parser: InputParser, input_str: str) - [ "[3x]", "[5d]", + "[2s]", + "[10k]", "[1.5days]", ], ) @@ -221,11 +241,14 @@ def test_parse_duration_multiple_brackets_uses_first(parser: InputParser) -> Non def test_parse_duration_case_insensitive(parser: InputParser) -> None: assert parser.parse_duration("[2H]") == 120 assert parser.parse_duration("[2Hr]") == 120 + assert parser.parse_duration("[2HRS]") == 120 assert parser.parse_duration("[30MIN]") == 30 + assert parser.parse_duration("[30Minutes]") == 30 def test_parse_duration_with_spaces(parser: InputParser) -> None: assert parser.parse_duration("[ 2 h ]") == 120 + assert parser.parse_duration("[\t30\tm\t]") == 30 def test_parse_duration_no_brackets(parser: InputParser) -> None: @@ -238,7 +261,10 @@ def test_parse_duration_no_brackets(parser: InputParser) -> None: [ ("#tag1 #tag2 #tag3", {"tag1", "tag2", "tag3"}), ("#TAG #Tag #tag", {"TAG", "Tag", "tag"}), # Case preserved + ("#a #b #c #a #b", {"a", "b", "c"}), + ("#123 #456", {"123", "456"}), ("#under_score #dash-tag", {"under_score", "dash-tag"}), + ("#mix123abc", {"mix123abc"}), ], ) def test_parse_tags_various_formats(parser: InputParser, input_str: str, expected: set) -> None: @@ -249,7 +275,9 @@ def test_parse_tags_various_formats(parser: InputParser, input_str: str, expecte "input_str", [ "no tags here", + "has # space", "##", + "###", "#", ], ) @@ -261,7 +289,11 @@ def test_parse_tags_no_valid_tags(parser: InputParser, input_str: str) -> None: "input_str, expected", [ ("#tag!", {"tag"}), # Stops at special char + ("#tag@email", {"tag"}), + ("#tag.with.dots", {"tag"}), ("#tag(parentheses)", {"tag"}), + ("#tag[brackets]", {"tag"}), + ("#tag{braces}", {"tag"}), ], ) def test_parse_tags_with_special_chars(parser: InputParser, input_str: str, expected: set) -> None: @@ -284,7 +316,10 @@ def test_parse_tags_empty_string(parser: InputParser) -> None: "input_str, expected", [ ("work [30m] #tag", "work"), + ("#tag1 #tag2 work", "work"), + ("work #tag [60m]", "work"), ("[2h] #dev work #qa [30m]", "work [30m]"), + (" multiple spaces ", "multiple spaces"), ("\ttabs\tand\nnewlines\n", "tabs and newlines"), ], ) From 76c3ae253c8b7d13c02cc0e215c09289b048608f Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Tue, 3 Feb 2026 22:39:37 -0500 Subject: [PATCH 32/33] Update lockfile to fix pip-audit --- uv.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index d673c91..d74b580 100644 --- a/uv.lock +++ b/uv.lock @@ -990,14 +990,14 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, ] [[package]] @@ -1273,11 +1273,11 @@ wheels = [ [[package]] name = "pip" -version = "25.3" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/c2/65686a7783a7c27a329706207147e82f23c41221ee9ae33128fc331670a0/pip-26.0.tar.gz", hash = "sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa", size = 1812654, upload-time = "2026-01-31T01:40:54.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, + { url = "https://files.pythonhosted.org/packages/69/00/5ac7aa77688ec4d34148b423d34dc0c9bc4febe0d872a9a1ad9860b2f6f1/pip-26.0-py3-none-any.whl", hash = "sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754", size = 1787564, upload-time = "2026-01-31T01:40:52.252Z" }, ] [[package]] From eabaea1c96e5912cd795a9a41b7f372094175662 Mon Sep 17 00:00:00 2001 From: Visesh Rajendraprasad Date: Mon, 9 Feb 2026 19:42:28 -0500 Subject: [PATCH 33/33] Normalize fetch tag filters and document behavior --- README.md | 4 +++- tests/test_cli.py | 1 + tests/test_workedon.py | 5 +++++ workedon/cli.py | 5 ++++- workedon/workedon.py | 9 +++++---- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9b857eb..699b113 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,8 @@ Options: -g, --no-page Don't page the output. -l, --text-only Output the work log text only. -T, --tag TEXT Tag to filter by. Can be used multiple times to filter - by multiple tags. + by multiple tags. Tags are normalized (trimmed and + lowercased). -D, --duration TEXT Duration to filter by. [default: ""] --date-format TEXT Set the date format of the output. Must be a valid Python strftime string. [env var: @@ -189,6 +190,7 @@ Options: - Tags can contain alphanumeric characters, underscores, and hyphens only. - Query logged work by tags using the `--tag/-T` option. Using it multiple times will match any of the specified tags. + - Filter tags are normalized the same way as saved tags (trimmed, lowercased, and empty values are ignored). - Specify duration while adding work. - Duration can be specified in two ways: - The `--duration/-D` option, e.g. `--duration 1h30m` or `--duration 90m`. diff --git a/tests/test_cli.py b/tests/test_cli.py index 36f93e6..c909a84 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -507,6 +507,7 @@ def test_weird_tag_parsing(runner: CliRunner, input_text: str, expected_tags: se ("working on #devtools", ["--tag", "devtools"], True), ("#in-progress cleanup", ["--tag", "in-progress"], True), ("writing code #DEV", ["--tag", "dev"], True), # case-insensitive + ("writing code #DEV", ["--tag", " dev "], True), # surrounding whitespace ignored ("refactoring #code_review", ["--tag", "code_review"], True), ("invalid ##doubletag", ["--tag", "doubletag"], True), ("emoji #🔥", ["--tag", "🔥"], False), diff --git a/tests/test_workedon.py b/tests/test_workedon.py index 39672df..b05d899 100644 --- a/tests/test_workedon.py +++ b/tests/test_workedon.py @@ -138,6 +138,11 @@ def test_save_work_ignores_empty_tag_option() -> None: assert WorkTag.select().count() == 1 +def test_normalize_tags_trims_lowercases_dedupes_and_drops_empty() -> None: + result = workedon._normalize_tags([" Dev ", "DEV", "dev", "", " "]) + assert result == {"dev"} + + def test_save_work_raises_cannot_save_on_failure( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/workedon/cli.py b/workedon/cli.py index 5156b0c..140d771 100644 --- a/workedon/cli.py +++ b/workedon/cli.py @@ -417,7 +417,10 @@ def workedon(stuff: tuple[str, ...], **kwargs: Any) -> None: multiple=True, required=False, type=click.STRING, - help="Tag to filter by. Can be used multiple times to filter by multiple tags.", + help=( + "Tag to filter by. Can be used multiple times to filter by multiple tags. " + "Tags are normalized (trimmed and lowercased)." + ), ) @click.option( "--duration", diff --git a/workedon/workedon.py b/workedon/workedon.py index a18e0d5..7d42e3e 100644 --- a/workedon/workedon.py +++ b/workedon/workedon.py @@ -183,10 +183,11 @@ def fetch_work( else: # tag if tags: - normalized = [t.lower() for t in tags] - tag_ids = Tag.select(Tag.uuid).where(Tag.name.in_(normalized)) - work_ids = WorkTag.select(WorkTag.work).where(WorkTag.tag.in_(tag_ids)) - work_set = work_set.where(Work.uuid.in_(work_ids)) + normalized_tags = _normalize_tags(tags) + if normalized_tags: + tag_ids = Tag.select(Tag.uuid).where(Tag.name.in_(normalized_tags)) + work_ids = WorkTag.select(WorkTag.work).where(WorkTag.tag.in_(tag_ids)) + work_set = work_set.where(Work.uuid.in_(work_ids)) # duration if duration: # Match optional comparison operator and value (e.g., '>=3h', '<= 45min', '2h')