From 3ced18cdef51f10fd8dbdefc8efbe89749cecb6f Mon Sep 17 00:00:00 2001 From: DaxServer Date: Thu, 16 Apr 2026 20:36:44 +0200 Subject: [PATCH 1/3] feat: add FlickreviewR bot for Flickypedia uploads Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 4 +- Procfile | 1 + poetry.lock | 46 +++++++-------- pyproject.toml | 13 +++-- src/wikibots/flickr.py | 12 +++- src/wikibots/flickrreview.py | 99 +++++++++++++++++++++++++++++++++ src/wikibots/lib/bot.py | 35 +++++++++++- tests/test_flickr_rate_limit.py | 12 ++-- 8 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 src/wikibots/flickrreview.py diff --git a/CLAUDE.md b/CLAUDE.md index e620eef..9454db4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,8 @@ wikibots enriches Wikimedia Commons files with structured data (SDC) pulled from - `src/wikibots/lib/claims.py` — `ClaimsMixin` with all `create_*` claim methods and hook stubs (`hook_creator_claim`, `hook_creator_target`, `hook_depicts_claim`, `hook_source_claim`). - `src/wikibots/lib/bot.py` — `BaseBot(ClaimsMixin)`. Handles OAuth2 auth, Redis caching, HTTP sessions, file metadata, Commons/Wikidata API calls, and the main run loop. - `src/wikibots/lib/wikidata.py` — Named constants for all Wikidata property (P-numbers) and entity (Q-numbers) IDs used across bots. -- `src/wikibots/flickr.py`, `inaturalist.py`, `pas.py`, `youtube.py` — Each bot overrides `treat_page()` and implements hooks for service-specific claim qualifiers. +- `src/wikibots/flickr.py`, `inaturalist.py`, `pas.py`, `youtube.py` — SDC bots: override `treat_page()` and implement hooks for service-specific claim qualifiers. Use `save()` to write claims via `wbeditentity`. +- `src/wikibots/flickrreview.py` — Wikitext bot: adds `{{Flickrreview}}` to Flickypedia uploads. Uses the `edit` API action directly (not `wbeditentity`). Override `skip_page()` for pre-loop filtering with Redis caching. ### Data flow @@ -73,6 +74,7 @@ The `status` parameter determines the review outcome. Only `pass` means the lice ### Constraints +- Commons API with `formatversion: 2` returns `pages` as a **list**, not a dict keyed by pageid. Use `pages[0]` — not `pages.get(pageid)`. - `redis` must stay on `<6.0.0` — Toolforge runs Redis server 6.0.16, and redis-py 7.x requires Redis 7.2+. - `flickr_api` v3 (current) uses `PermissionDenied` for private/inaccessible photos — `PhotoIsPrivate` no longer exists. - pywikibot config is accessed via `pwb.config` where `pwb` is imported from `pywikibot.scripts.wrapper`. Importing through the wrapper triggers proper pywikibot initialization — using `pywikibot.config` directly does not work. The ty `possibly-missing-submodule` warnings on `pwb.config` are expected (exit 0). diff --git a/Procfile b/Procfile index 5954abb..67b117f 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,5 @@ flickr: python -m wikibots.flickr +flickrreview: python -m wikibots.flickrreview inaturalist: python -m wikibots.inaturalist pas: python -m wikibots.pas youtube: python -m wikibots.youtube diff --git a/poetry.lock b/poetry.lock index 73f82b0..e320b99 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. [[package]] name = "anyio" @@ -738,14 +738,14 @@ test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest- [[package]] name = "packaging" -version = "26.0" +version = "26.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, ] [[package]] @@ -1094,29 +1094,29 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "ty" -version = "0.0.28" +version = "0.0.31" description = "An extremely fast Python type checker, written in Rust." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "ty-0.0.28-py3-none-linux_armv6l.whl", hash = "sha256:6dbfb27524195ab1715163d7be065cc45037509fe529d9763aff6732c919f0d8"}, - {file = "ty-0.0.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c72a899ba94f7438bd07e897a84b36526b385aaf01d6f3eb6504e869232b3a6"}, - {file = "ty-0.0.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eef67f9cdfd31677bde801b611741dde779271ec6f471f818c7c6eccf515237f"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70e7b98a91d8245641be1e4b55af8bc9b1ae82ec189794d35e14e546f1e15e66"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd83d4ad9f99078b830aabb47792fac6dc39368bb0f72f3cc14607173ed6e25"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0172984fc2fcd3e47ccd5da69f36f632cddc410f9a093144a05ad07d67cf06ed"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0bbf47d2bea82a09cab2ca4f48922d6c16a36608447acdc64163cd19beb28d3"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1774c9a0fb071607e3bdfa0ce8365488ac46809fc04ad1706562a8709a023247"}, - {file = "ty-0.0.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2849d6d212af78175430e8cc51a962a53851458182eb44a981b0e3981163177"}, - {file = "ty-0.0.28-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3c576c15b867b3913c4a1d9be30ade4682303e24a576d2cc99bfd8f25ae838e9"}, - {file = "ty-0.0.28-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e5f13d10b3436bee3ea35851e5af400123f6693bfae48294ddfbbf553fa51ef"}, - {file = "ty-0.0.28-py3-none-musllinux_1_2_i686.whl", hash = "sha256:759db467e399faedc7d5f1ca4b383dd8ecc71d7d79b2ca6ea6db4ac8e643378a"}, - {file = "ty-0.0.28-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0cd44e3c857951cbf3f8647722ca87475614fac8ac0371eb1f200a942315a2c2"}, - {file = "ty-0.0.28-py3-none-win32.whl", hash = "sha256:88e2c784ec5e0e2fb01b137d92fd595cdc27b98a553f4bb34b8bf138bac1be1e"}, - {file = "ty-0.0.28-py3-none-win_amd64.whl", hash = "sha256:faaffbef127cb67560ad6dbc6a8f8845a4033b818bcc78ad7af923e02df199db"}, - {file = "ty-0.0.28-py3-none-win_arm64.whl", hash = "sha256:34a18ea09ee09612fb6555deccf1eed810e6f770b61a41243b494bcb7f624a1c"}, - {file = "ty-0.0.28.tar.gz", hash = "sha256:1fbde7bc5d154d6f047b570d95665954fa83b75a0dce50d88cf081b40a27ea32"}, + {file = "ty-0.0.31-py3-none-linux_armv6l.whl", hash = "sha256:761651dc17ad7bc0abfc1b04b3f0e84df263ed435d34f29760b3da739ab02d35"}, + {file = "ty-0.0.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c529922395a07231c27488f0290651e05d27d149f7e0aa807678f1f7e9c58a5e"}, + {file = "ty-0.0.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f345df2b87d747859e72c2cbc9be607ea1bbc8bc93dd32fa3d03ea091cb4fee"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b207eddcfbafd376132689d3435b14efcb531289cb59cd961c6a611133bd54"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:663778b220f357067488ce68bfc52335ccbd161549776f70dcbde6bbde82f77a"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3506cfe87dfade0fb2960dd4fffd4fd8089003587b3445c0a1a295c9d83764fb"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b3f3d8492f08e81916026354c1d1599e9ddfa1241804141a74d5662fc710085"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97de32ee6a619393a4c495e056a1c547de7877510f3152e61345c71d774d2d0"}, + {file = "ty-0.0.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c906354ce441e342646582bc9b8f48a676f79f3d061e25de15ff870e015ca14e"}, + {file = "ty-0.0.31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:275bb7c82afcbf89fe2dbef1b2692f2bc98451f1ee2c8eb809ddd91317822388"}, + {file = "ty-0.0.31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:405da247027c6efd1e264886b6ac4a86ab3a4f09200b02e33630efe85f119e53"}, + {file = "ty-0.0.31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54d9835608eed196853d6643f645c50ce83bcc7fe546cdb3e210c1bcf7c58c09"}, + {file = "ty-0.0.31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ee11be9b07e8c0c6b455ff075a0abe4f194de9476f57624db98eec9df618355"}, + {file = "ty-0.0.31-py3-none-win32.whl", hash = "sha256:7286587aacf3eef0956062d6492b893b02f82b0f22c5e230008e13ff0d216a8b"}, + {file = "ty-0.0.31-py3-none-win_amd64.whl", hash = "sha256:81134e25d2a2562ab372f24de8f9bd05034d27d30377a5d7540f259791c6234c"}, + {file = "ty-0.0.31-py3-none-win_arm64.whl", hash = "sha256:e9cb15fad26545c6a608f40f227af3a5513cb376998ca6feddd47ca7d93ffafa"}, + {file = "ty-0.0.31.tar.gz", hash = "sha256:4a4094292d9671caf3b510c7edf36991acd9c962bb5d97205374ffed9f541c45"}, ] [[package]] @@ -1152,4 +1152,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "ca07fa0b2229a40abdcc2caf44d0d8d5530970b44b7444b82379cb3e3fff2d6d" +content-hash = "1a93952d99b84465ffbed30508c9366323911b200eba5cc721bfe62731f1d4dd" diff --git a/pyproject.toml b/pyproject.toml index ec028f9..760e8e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,15 +7,15 @@ authors = [ readme = "README.md" requires-python = ">=3.13,<4.0" dependencies = [ - "python-dateutil (>=2.9.0.post0,<3.0.0)", "deepdiff (>=9.0.0,<10.0.0)", - "requests-oauthlib (>=2.0.0,<3.0.0)", "google-api-python-client (>=2.166.0,<3.0.0)", "flickr-photos-api (>=3.0.0,<4.0.0)", "flickr-url-parser (>=1.11.0,<2.0.0)", - "redis (>=5.2.1,<6.0.0)", "mwparserfromhell (>=0.7.2,<1.0.0)", + "python-dateutil (>=2.9.0.post0,<3.0.0)", + "redis (>=5.2.1,<6.0.0)", "requests (>=2.32.0,<3.0.0)", + "requests-oauthlib (>=2.0.0,<3.0.0)", ] [tool.poetry] @@ -23,6 +23,7 @@ packages = [{include = "wikibots", from = "src"}] [project.scripts] flickr = "wikibots.flickr:main" +flickrreview = "wikibots.flickrreview:main" inaturalist = "wikibots.inaturalist:main" pas = "wikibots.pas:main" youtube = "wikibots.youtube:main" @@ -43,12 +44,12 @@ exclude = [ [dependency-groups] dev = [ - "ruff (>=0.15.9,<0.16.0)", - "ty (>=0.0.28,<0.0.29)", "isort (>=8.0.1,<9.0.0)", "pytest (>=9.0.3,<10.0.0)", "pytest-mock (>=3.15.1,<4.0.0)", - "pytest-timeout (>=2.4.0,<3.0.0)" + "pytest-timeout (>=2.4.0,<3.0.0)", + "ruff (>=0.15.10,<0.16.0)", + "ty (>=0.0.31,<0.0.32)" ] diff --git a/src/wikibots/flickr.py b/src/wikibots/flickr.py index 6f0b78d..7939978 100644 --- a/src/wikibots/flickr.py +++ b/src/wikibots/flickr.py @@ -144,7 +144,9 @@ def get_flickr_photo(self, flickr_photo_id: str) -> None: while True: try: start = perf_counter() - self.photo = self.flickr_api.get_single_photo_info(photo_id=flickr_photo_id) + self.photo = self.flickr_api.get_single_photo_info( + photo_id=flickr_photo_id + ) logger.info( f"Retrieved Flickr photo in {(perf_counter() - start) * 1000:.0f} ms" ) @@ -159,9 +161,13 @@ def get_flickr_photo(self, flickr_photo_id: str) -> None: return delay = next(delays, None) if delay is None: - logger.critical(f"[{flickr_photo_id}] Rate limit exhausted after all retries") + logger.critical( + f"[{flickr_photo_id}] Rate limit exhausted after all retries" + ) raise RateLimitExhausted - logger.warning(f"[{flickr_photo_id}] Rate limited, retrying in {delay}s") + logger.warning( + f"[{flickr_photo_id}] Rate limited, retrying in {delay}s" + ) time.sleep(delay) except Exception as e: logger.error(f"[{flickr_photo_id}] {e}") diff --git a/src/wikibots/flickrreview.py b/src/wikibots/flickrreview.py new file mode 100644 index 0000000..c0a357a --- /dev/null +++ b/src/wikibots/flickrreview.py @@ -0,0 +1,99 @@ +import logging +from typing import Any + +from wikibots.lib.bot import BaseBot + +logger = logging.getLogger(__name__) + +FLICKRREVIEW_TEMPLATE = "{{Flickrreview}}\n" + + +class FlickreviewrBot(BaseBot): + redis_prefix = "flickrreview" + summary = ( + "add [[Template:Flickrreview|Flickrreview]] template to Flickypedia uploads. test run." + ) + search_query = ( + 'file: incategory:"Uploads using Flickypedia" -hastemplate:Flickrreview' + ) + + def skip_page(self, page: dict[str, Any]) -> bool: + mid = f"M{page['pageid']}" + redis_key = f"{self.redis_prefix}:commons:{mid}" + + if self.has_template(page, "FlickreviewR"): + logger.info(f"Skipping: FlickreviewR already present on {page['title']}") + self.redis.set(redis_key, 1) + return True + + if self.has_user_edited(page): + logger.info(f"Skipping: bot has already edited {page['title']}") + self.redis.set(redis_key, 1) + return True + + return False + + def treat_page(self) -> None: + assert self.wiki_properties + + self.parse_wikicode() + assert self.wiki_properties.wikicode + + wikicode = self.wiki_properties.wikicode + templates = wikicode.filter_templates() + + flickypedia = next( + (t for t in templates if t.name.strip() == "Uploaded with Flickypedia"), + None, + ) + if flickypedia is None: + logger.warning( + f"No Flickypedia template found on {self.current_page['title']}" + ) + self.redis.set(self.wiki_properties.redis_key, 1) + return + + wikicode.insert_before(flickypedia, FLICKRREVIEW_TEMPLATE) + + new_text = str(wikicode).replace("\n\n{{Flickrreview}}", "\n{{Flickrreview}}") + + if self.dry_run: + logger.info(f"Dry run: would edit {self.current_page['title']}") + logger.info(new_text) + return + + token = self._get_csrf_token() + response = self._commons_session.post( + "https://commons.wikimedia.org/w/api.php", + data={ + "action": "edit", + "pageid": self.current_page["pageid"], + "text": new_text, + "summary": self.summary, + "bot": "1", + "format": "json", + "token": token, + }, + timeout=60.0, + ) + response.raise_for_status() + result = response.json() + if "error" in result: + logger.critical( + f"API error editing {self.current_page['title']}: {result['error']}" + ) + return + + logger.info(f"Added Flickrreview to {self.current_page['title']}") + self.redis.set(self.wiki_properties.redis_key, 1) + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" + ) + FlickreviewrBot().run() + + +if __name__ == "__main__": + main() diff --git a/src/wikibots/lib/bot.py b/src/wikibots/lib/bot.py index 3b1b5a5..42d14c7 100644 --- a/src/wikibots/lib/bot.py +++ b/src/wikibots/lib/bot.py @@ -42,7 +42,7 @@ def __init__(self) -> None: self.current_page: dict[str, Any] = {} self.wiki_properties: WikiProperties | None = None - self._username = os.getenv("PWB_USERNAME", "") + self._username = os.getenv("PWB_USERNAME", "DaxServer") self.user_agent = f"{self._username} / Wikimedia Commons / {os.getenv('EMAIL')}" auth = OAuth1( @@ -149,6 +149,39 @@ def run(self) -> None: def skip_page(self, page: dict[str, Any]) -> bool: return False + def has_template(self, page: dict[str, Any], template_name: str) -> bool: + """Check if a page contains a specific template.""" + result = self._commons_api( + { + "action": "query", + "pageids": page["pageid"], + "prop": "templates", + "tltemplates": f"Template:{template_name}", + "tllimit": 1, + "formatversion": 2, + } + ) + pages = result.get("query", {}).get("pages", []) + templates = pages[0].get("templates", []) if pages else [] + return len(templates) > 0 + + def has_user_edited(self, page: dict[str, Any]) -> bool: + """Check if the bot user has previously edited this page.""" + result = self._commons_api( + { + "action": "query", + "pageids": page["pageid"], + "prop": "revisions", + "rvuser": self._username, + "rvlimit": 1, + "rvprop": "ids", + "formatversion": 2, + } + ) + pages = result.get("query", {}).get("pages", []) + revisions = pages[0].get("revisions", []) if pages else [] + return len(revisions) > 0 + def treat_page(self) -> None: pass diff --git a/tests/test_flickr_rate_limit.py b/tests/test_flickr_rate_limit.py index 2e08e16..f151bf3 100644 --- a/tests/test_flickr_rate_limit.py +++ b/tests/test_flickr_rate_limit.py @@ -9,9 +9,11 @@ def make_bot() -> FlickrBot: """Create a FlickrBot with all external dependencies mocked.""" - with patch("wikibots.lib.bot.Redis") as mock_redis_cls, \ - patch("wikibots.lib.bot.requests.Session"), \ - patch("wikibots.flickr.FlickrApi"): + with ( + patch("wikibots.lib.bot.Redis") as mock_redis_cls, + patch("wikibots.lib.bot.requests.Session"), + patch("wikibots.flickr.FlickrApi"), + ): mock_redis_cls.return_value.ping.return_value = True bot = FlickrBot() bot.redis = MagicMock() @@ -21,7 +23,9 @@ def make_bot() -> FlickrBot: def make_429_error() -> httpx.HTTPStatusError: response = MagicMock(spec=httpx.Response) response.status_code = 429 - return httpx.HTTPStatusError("429 Too Many Requests", request=MagicMock(), response=response) + return httpx.HTTPStatusError( + "429 Too Many Requests", request=MagicMock(), response=response + ) def test_rate_limit_retries_with_delays_then_raises(mocker): From 322b769137db0c56aa277bfb29bc8843c16fe4db Mon Sep 17 00:00:00 2001 From: DaxServer Date: Thu, 16 Apr 2026 20:42:15 +0200 Subject: [PATCH 2/3] fix: address PR review comments Co-Authored-By: Claude Sonnet 4.6 --- src/wikibots/flickrreview.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/wikibots/flickrreview.py b/src/wikibots/flickrreview.py index c0a357a..36e62b8 100644 --- a/src/wikibots/flickrreview.py +++ b/src/wikibots/flickrreview.py @@ -11,7 +11,7 @@ class FlickreviewrBot(BaseBot): redis_prefix = "flickrreview" summary = ( - "add [[Template:Flickrreview|Flickrreview]] template to Flickypedia uploads. test run." + "add [[Template:Flickrreview|Flickrreview]] template to Flickypedia uploads" ) search_query = ( 'file: incategory:"Uploads using Flickypedia" -hastemplate:Flickrreview' @@ -62,22 +62,17 @@ def treat_page(self) -> None: logger.info(new_text) return - token = self._get_csrf_token() - response = self._commons_session.post( - "https://commons.wikimedia.org/w/api.php", + result = self._commons_api( + params={"action": "edit"}, + method="POST", data={ - "action": "edit", "pageid": self.current_page["pageid"], "text": new_text, "summary": self.summary, "bot": "1", - "format": "json", - "token": token, + "token": self._get_csrf_token(), }, - timeout=60.0, ) - response.raise_for_status() - result = response.json() if "error" in result: logger.critical( f"API error editing {self.current_page['title']}: {result['error']}" From f51a499d53d7c5b9f1507489afe25bc87deffade Mon Sep 17 00:00:00 2001 From: DaxServer Date: Fri, 17 Apr 2026 18:15:05 +0200 Subject: [PATCH 3/3] chore: update deps --- poetry.lock | 38 +++++++++++++++++++------------------- pyproject.toml | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index e320b99..28835ea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1020,30 +1020,30 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "ruff" -version = "0.15.10" +version = "0.15.11" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f"}, - {file = "ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e"}, - {file = "ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609"}, - {file = "ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07"}, - {file = "ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48"}, - {file = "ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5"}, - {file = "ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed"}, - {file = "ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188"}, - {file = "ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e"}, + {file = "ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7"}, + {file = "ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e"}, + {file = "ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431"}, + {file = "ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19"}, + {file = "ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890"}, + {file = "ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5"}, + {file = "ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0"}, + {file = "ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c"}, + {file = "ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3"}, + {file = "ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3"}, + {file = "ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4"}, + {file = "ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 760e8e7..14477e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dev = [ "pytest-mock (>=3.15.1,<4.0.0)", "pytest-timeout (>=2.4.0,<3.0.0)", "ruff (>=0.15.10,<0.16.0)", - "ty (>=0.0.31,<0.0.32)" + "ty (>=0.0.31,<0.0.32)", ]