diff --git a/src/amcrest2mqtt/app.py b/src/amcrest2mqtt/app.py index 0a16682..eb5c738 100644 --- a/src/amcrest2mqtt/app.py +++ b/src/amcrest2mqtt/app.py @@ -34,10 +34,10 @@ async def async_main() -> int: async with Amcrest2Mqtt(args=args) as amcrest2mqtt: await amcrest2mqtt.main_loop() except ConfigError as err: - logger.error(f"fatal config error was found: {err}") + logger.error(f"fatal config error was found: {err!r}") return 1 except MqttError as err: - logger.error(f"mqtt service problems: {err}") + logger.error(f"mqtt service problems: {err!r}") return 1 except KeyboardInterrupt: logger.warning("shutdown requested (Ctrl+C). exiting gracefully...") @@ -46,7 +46,7 @@ async def async_main() -> int: logger.warning("main loop cancelled.") return 1 except Exception as err: - logger.error(f"unhandled exception: {err}", exc_info=True) + logger.error(f"unhandled exception: {err!r}", exc_info=True) return 1 finally: logger.info("amcrest2mqtt stopped.") diff --git a/src/amcrest2mqtt/base.py b/src/amcrest2mqtt/base.py index 1bf5003..d206eeb 100644 --- a/src/amcrest2mqtt/base.py +++ b/src/amcrest2mqtt/base.py @@ -94,14 +94,14 @@ class Base: await cast(Any, self).publish_service_availability("offline") cast(Any, self).mqttc.loop_stop() except Exception as err: - self.logger.debug(f"mqtt loop_stop failed: {err}") + self.logger.debug(f"mqtt loop_stop failed: {err!r}") if cast(Any, self).mqttc.is_connected(): try: cast(Any, self).mqttc.disconnect() self.logger.info("disconnected from MQTT broker") except Exception as err: - self.logger.warning(f"error during MQTT disconnect: {err}") + self.logger.warning(f"error during MQTT disconnect: {err!r}") self.logger.info("exiting gracefully") diff --git a/src/amcrest2mqtt/interface.py b/src/amcrest2mqtt/interface.py index cc0291b..7c193fa 100644 --- a/src/amcrest2mqtt/interface.py +++ b/src/amcrest2mqtt/interface.py @@ -58,6 +58,7 @@ class AmcrestServiceProtocol(Protocol): async def get_ip_address(self, string: str) -> str: ... async def get_motion_detection(self, device_id: str) -> bool: ... async def get_privacy_mode(self, device_id: str) -> bool: ... + async def get_recorded_file(self, device_id: str, file: str, encode: bool = True) -> str | None: ... async def get_snapshot_from_device(self, device_id: str) -> str | None: ... async def get_storage_stats(self, device_id: str) -> dict[str, str | float]: ... async def handle_device_command(self, device_id: str, handler: str, message: Any) -> None: ... @@ -108,7 +109,6 @@ class AmcrestServiceProtocol(Protocol): def get_device_name_slug(self, device_id: str) -> str: ... def get_device_state_topic(self, device_id: str, mode_name: str = "") -> str: ... def get_next_event(self) -> dict[str, Any] | None: ... - def get_recorded_file(self, device_id: str, file: str, encode: bool = True) -> str | None: ... def get_snapshot(self, device_id: str) -> str | None: ... def handle_signal(self, signum: int, _: FrameType | None) -> Any: ... def heartbeat_ready(self) -> None: ... diff --git a/src/amcrest2mqtt/mixins/amcrest.py b/src/amcrest2mqtt/mixins/amcrest.py index 0fe2546..24ac4b8 100644 --- a/src/amcrest2mqtt/mixins/amcrest.py +++ b/src/amcrest2mqtt/mixins/amcrest.py @@ -17,13 +17,20 @@ class AmcrestMixin: seen_devices: set[str] = set() # Build all components concurrently - tasks = [self.build_component(device) for device in amcrest_devices.values()] - results = await asyncio.gather(*tasks, return_exceptions=True) + tasks = [(device, self.build_component(device)) for device in amcrest_devices.values()] + results = await asyncio.gather(*[task[1] for task in tasks], return_exceptions=True) # Collect successful device IDs - for result in results: + for (device, _), result in zip(tasks, results): if isinstance(result, Exception): - self.logger.error(f"error during build_component: {result}", exc_info=True) + device_name = device.get("device_name", "unknown") + device_id = device.get("serial_number", "unknown") + exception_type = type(result).__name__ + self.logger.error( + f"error during build_component for device '{device_name}' ({device_id}): " + f"{exception_type}: {result}", + exc_info=True + ) elif result and isinstance(result, str): seen_devices.add(result) @@ -72,7 +79,7 @@ class AmcrestMixin: return "" async def build_camera(self: Amcrest2Mqtt, camera: dict) -> str: - raw_id = cast(str, camera["serial_number"]) + raw_id = cast(str, camera["serial_number"]).strip() device_id = raw_id rtc_url = "" @@ -94,7 +101,7 @@ class AmcrestMixin: "name": camera["device_name"], "identifiers": [ self.mqtt_helper.device_slug(device_id), - camera["serial_number"], + raw_id, ], "manufacturer": camera["vendor"], "model": camera["device_type"], @@ -288,34 +295,14 @@ class AmcrestMixin: "icon": "mdi:person", } - # defaults - which build_device_states doesn't update (events do) - self.upsert_state( - device_id, - internal={"discovered": False}, - camera={"video": None}, - image={"snapshot": None, "motion_snapshot": None}, - switch={"save_recordings": "ON" if "path" in self.config["media"] else "OFF"}, - binary_sensor={ - "motion": False, - "doorbell": False, - "human": False, - }, - sensor={ - "motion_detection": "ON", - "privacy": "OFF", - "storage_used": 0, - "storage_total": 0, - "storage_used_pct": 0, - "motion_region": "n/a", - }, - ) + self.upsert_state(device_id, internal={}) self.upsert_device(device_id, component=device, cmps={k: v for k, v in device["cmps"].items()}) await self.build_device_states(device_id) if not self.is_discovered(device_id): self.logger.info(f'added new camera: "{camera["device_name"]}" {camera["vendor"]} {camera["device_type"]}] ({device_id})') + await self.publish_device_discovery(device_id) - await self.publish_device_discovery(device_id) await self.publish_device_availability(device_id, online=True) await self.publish_device_state(device_id) diff --git a/src/amcrest2mqtt/mixins/amcrest_api.py b/src/amcrest2mqtt/mixins/amcrest_api.py index 9393be5..ff9b688 100644 --- a/src/amcrest2mqtt/mixins/amcrest_api.py +++ b/src/amcrest2mqtt/mixins/amcrest_api.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2025 Jeff Culverhouse +import re from amcrest import AmcrestCamera, ApiWrapper from amcrest.exceptions import LoginError, AmcrestError, CommError import asyncio @@ -12,11 +13,6 @@ from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from amcrest2mqtt.interface import AmcrestServiceProtocol as Amcrest2Mqtt -SNAPSHOT_TIMEOUT_S = 20 -SNAPSHOT_MAX_TRIES = 3 -SNAPSHOT_BASE_BACKOFF_S = 8 - - class AmcrestAPIMixin: def increase_api_calls(self: Amcrest2Mqtt) -> None: if not self.last_call_date or self.last_call_date.date() != datetime.now().date(): @@ -40,7 +36,7 @@ class AmcrestAPIMixin: index += 1 await asyncio.gather(*tasks) - self.logger.info("connecting to hosts done") + self.logger.info("connecting to Amcrest hosts done") return {d: self.amcrest_devices[d]["config"] for d in self.amcrest_devices.keys()} async def get_camera(self: Amcrest2Mqtt, host: str) -> ApiWrapper: @@ -51,22 +47,15 @@ class AmcrestAPIMixin: config["username"], config["password"], verbose=False, - retries_connection=0, # don’t multiply wall time at startup + retries_connection=0, timeout_protocol=(4.0, 4.0), # (connect, read) in seconds ).camera async def get_device(self: Amcrest2Mqtt, host: str, device_name: str, index: int) -> None: def clean_value(value: str | Sequence[str], prefix: str = "") -> str: - # Normalize to a string first if not isinstance(value, str): - # Handle list/tuple cases - if isinstance(value, Sequence) and len(value) > 0: - value = value[0] - else: - # Graceful fallback if value is empty or weird - return "" - - # At this point, value is guaranteed to be a str + value = value[0] if isinstance(value, Sequence) else "" + if prefix and value.startswith(prefix): value = value[len(prefix) :] return value.strip() @@ -81,10 +70,10 @@ class AmcrestAPIMixin: self.logger.error(f'invalid username/password to connect to device "{host}", fix in config.yaml') return except AmcrestError as err: - self.logger.error(f'unexpected error connecting to device "{host}", check config.yaml: {err}') + self.logger.error(f'unexpected error connecting to device "{host}", check config.yaml: {err!r}') return except Exception as err: - self.logger.error(f"error connecting to {host}: {err}") + self.logger.error(f"error connecting to {host}: {err!r}") return ( @@ -161,51 +150,65 @@ class AmcrestAPIMixin: } def reboot_device(self: Amcrest2Mqtt, device_id: str) -> None: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None device = self.amcrest_devices[device_id] if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return None + response = device["camera"].reboot().strip() - self.logger.info(f"sent REBOOT signal to {self.get_device_name(device_id)}, {response}") + self.logger.debug(f"sent reboot signal to {self.get_device_name(device_id)}, {response}") if response == "OK": self.upsert_state(device_id, internal={"reboot": datetime.now()}) + self.logger.info(f"rebooted {self.get_device_name(device_id)}") + return + self.logger.error(f"failed to reboot {self.get_device_name(device_id)}: {response}") def is_rebooting(self: Amcrest2Mqtt, device_id: str) -> bool: states = self.states[device_id] if "reboot" not in states["internal"]: return False + reboot_time = states["internal"]["reboot"] if reboot_time + timedelta(minutes=2) > datetime.now(): return True + states["internal"].pop("reboot") if states["sensor"].get("event_text", "").startswith("Reboot"): self.upsert_state(device_id, sensor={"event_text": ""}) + return False # Storage stats ------------------------------------------------------------------------------- async def get_storage_stats(self: Amcrest2Mqtt, device_id: str) -> dict[str, str | float]: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return {} + device = self.amcrest_devices[device_id] states = self.states[device_id] - # return our last known state if we fail to get new stats + # return our last known state if something fails current: dict[str, str | float] = { "used_percent": states["sensor"]["storage_used_pct"], "used": states["sensor"]["storage_used"], "total": states["sensor"]["storage_total"], - } + } if "sensor" in states else {} if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return current try: - storage = await device["camera"].async_storage_all + storage = cast(dict, await device["camera"].async_storage_all) except CommError as err: - self.logger.error(f"failed to get storage stats from ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to get storage stats from ({self.get_device_name(device_id)}): {err!r}") return current except LoginError as err: - self.logger.error(f"failed to auth to ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to auth to ({self.get_device_name(device_id)}): {err!r}") return current self.increase_api_calls() @@ -219,59 +222,91 @@ class AmcrestAPIMixin: # Privacy config ------------------------------------------------------------------------------ async def get_privacy_mode(self: Amcrest2Mqtt, device_id: str) -> bool: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return False + device = self.amcrest_devices[device_id] states = self.states[device_id] - - # return our last known state if we fail to get new stats - current = True if states["sensor"]["privacy"] == "ON" else False + # return our last known state if something fails + current = True if "sensor" in states and states["sensor"].get("privacy", "OFF") == "ON" else False if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return current try: - privacy = await device["camera"].async_privacy_config() + response = await device["camera"].async_privacy_config() except CommError as err: - self.logger.error(f"failed to get privacy mode from ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to get privacy mode from ({self.get_device_name(device_id)}): {err!r}") return current except LoginError as err: - self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err!r}") return current self.increase_api_calls() - if not privacy or not isinstance(privacy, list) or len(privacy) < 1: + + privacy = response if isinstance(response, dict) else {} + if isinstance(response, str): + for line in response.strip().split("\n"): + line = line.strip() + if "=" in line: + key, value = line.split("=", 1) + privacy[key] = value + + # Extract Enable value from the parsed dictionary + enable_value = None + for key in privacy: + if key.endswith(".Enable") or key == "Enable": + enable_value = privacy[key] + break + + if enable_value is None: + self.logger.warning(f"failed to get privacy mode from ({self.get_device_name(device_id)}), got: {type(privacy)} with value: {privacy}") return current - privacy_mode = True if privacy[0].split("=")[1] == "true" else False + + privacy_mode = enable_value.lower() == "true" return privacy_mode async def set_privacy_mode(self: Amcrest2Mqtt, device_id: str, switch: bool) -> None: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + device = self.amcrest_devices[device_id] if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return None + camera = device["camera"] try: - response = str(await device["camera"].async_set_privacy(switch)).strip() - self.increase_api_calls() - self.logger.debug(f"set privacy_mode on {self.get_device_name(device_id)} to {switch}, got back: {response}") - if response == "OK": - self.upsert_state(device_id, switch={"privacy": "ON" if switch else "OFF"}) - await self.publish_device_state(device_id) + response = str(await camera.async_set_privacy(switch)).strip() except CommError as err: - self.logger.error(f"failed to set privacy mode on ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to set privacy mode on ({self.get_device_name(device_id)}): {err!r}") except LoginError as err: - self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err!r}") + self.increase_api_calls() + self.logger.debug(f"set privacy_mode on {self.get_device_name(device_id)} to {switch}, got back: {response}") + if response == "OK": + self.upsert_state(device_id, switch={"privacy": "ON" if switch else "OFF"}) + await self.publish_device_state(device_id) + return None + self.logger.error(f"failed to set privacy mode on ({self.get_device_name(device_id)}): {response}") return None # Motion detection config --------------------------------------------------------------------- async def get_motion_detection(self: Amcrest2Mqtt, device_id: str) -> bool: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return False + device = self.amcrest_devices[device_id] states = self.states[device_id] - # return our last known state if we fail to get new stats - current = True if states["sensor"]["motion_detection"] == "ON" else False + # return our last known state if something fails + current = True if "sensor" in states and states["sensor"].get("motion_detection", "OFF") == "ON" else False if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") @@ -280,51 +315,51 @@ class AmcrestAPIMixin: try: motion_detection = bool(await device["camera"].async_is_motion_detector_on()) except CommError as err: - self.logger.error(f"failed to get motion detection switch on ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to get motion detection switch on ({self.get_device_name(device_id)}): {err!r}") return current except LoginError as err: - self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err}") + self.logger.error(f"failed to auth to device ({self.get_device_name(device_id)}): {err!r}") return current self.increase_api_calls() return motion_detection async def set_motion_detection(self: Amcrest2Mqtt, device_id: str, switch: bool) -> None: - device = self.amcrest_devices[device_id] + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + device = self.amcrest_devices[device_id] if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return None + camera = device["camera"] try: - response = bool(await device["camera"].async_set_motion_detection(switch)) - self.increase_api_calls() - self.logger.debug(f"set motion_detection on {self.get_device_name(device_id)} to {switch}, got back: {response}") - if response: - self.upsert_state(device_id, switch={"motion_detection": "ON" if switch else "OFF"}) - await self.publish_device_state(device_id) + response = bool(await camera.async_set_motion_detection(switch)) except CommError: self.logger.error(f"failed to communicate with device ({self.get_device_name(device_id)}) to set motion detections") except LoginError: self.logger.error(f"failed to authenticate with device ({self.get_device_name(device_id)}) to set motion detections") + self.increase_api_calls() + self.logger.debug(f"set motion_detection on {self.get_device_name(device_id)} to {switch}, got back: {response}") + if response: + self.upsert_state(device_id, switch={"motion_detection": "ON" if switch else "OFF"}) + await self.publish_device_state(device_id) return None # Snapshots ----------------------------------------------------------------------------------- - async def collect_all_device_snapshots(self: Amcrest2Mqtt) -> None: - tasks = [] - for device_id in self.amcrest_devices: - if self.is_rebooting(device_id): - self.logger.debug(f"skipping snapshot for {self.get_device_name(device_id)}, still rebooting") - continue - tasks.append(self.get_snapshot_from_device(device_id)) - - if tasks: - await asyncio.gather(*tasks) - async def get_snapshot_from_device(self: Amcrest2Mqtt, device_id: str) -> str | None: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + device = self.amcrest_devices[device_id] + timeout = 20 + max_tries = 3 + base_backoff = 15 # Respect privacy mode (default False if missing) if device.get("privacy_mode", False): @@ -334,16 +369,19 @@ class AmcrestAPIMixin: if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return None + camera = device["camera"] - for attempt in range(1, SNAPSHOT_MAX_TRIES + 1): + for attempt in range(1, max_tries + 1): try: if self.is_rebooting(device_id): return None - image_bytes = await asyncio.wait_for(camera.async_snapshot(), timeout=SNAPSHOT_TIMEOUT_S) + + self.logger.debug(f"getting snapshot from {self.get_device_name(device_id)}") + image_bytes = await asyncio.wait_for(camera.async_snapshot(), timeout=timeout) self.increase_api_calls() if not image_bytes: - self.logger.warning(f"snapshot: empty image from {self.get_device_name(device_id)}") + self.logger.warning(f"snapshot: empty image from {self.get_device_name(device_id)}, ignoring") return None encoded_b = base64.b64encode(image_bytes) @@ -357,42 +395,42 @@ class AmcrestAPIMixin: self.logger.debug(f"got snapshot from {self.get_device_name(device_id)} {len(image_bytes)} raw bytes -> {len(encoded)} b64 chars") return encoded + except (CommError, LoginError, asyncio.TimeoutError, Exception) as err: + self.logger.debug(f"snapshot attempt {attempt}/{max_tries} failed for {self.get_device_name(device_id)}: {err!r}") + except asyncio.CancelledError: - # Let shutdown propagate + self.logger.debug(f"snapshot cancelled for {self.get_device_name(device_id)}, letting shutdown propagate") raise - except (CommError, LoginError, asyncio.TimeoutError) as err: - # Backoff with jitter before retrying - if attempt == SNAPSHOT_MAX_TRIES: - break - delay = SNAPSHOT_BASE_BACKOFF_S * (2 ** (attempt - 1)) - delay += random.uniform(0, 0.25) - self.logger.debug( - f"snapshot attempt {attempt}/{SNAPSHOT_MAX_TRIES} failed for {self.get_device_name(device_id)}: {err!r}; retrying in {delay:.2f}s" - ) - await asyncio.sleep(delay) - - # Any other unexpected exception: log and stop - except Exception as err: # noqa: BLE001 (log-and-drop is intentional here) - self.logger.exception(f"snapshot: unexpected error for {self.get_device_name(device_id)}: {err!r}") - return None + delay = base_backoff * (2 ** (attempt - 1)) + delay += random.uniform(0, 5) + await asyncio.sleep(delay) - self.logger.info(f"getting snapshot failed after {SNAPSHOT_MAX_TRIES} tries for {self.get_device_name(device_id)}") + self.logger.info(f"getting snapshot failed after {max_tries} tries for {self.get_device_name(device_id)}") return None def get_snapshot(self: Amcrest2Mqtt, device_id: str) -> str | None: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + return self.amcrest_devices[device_id]["snapshot"] if "snapshot" in self.devices[device_id] else None # Recorded file ------------------------------------------------------------------------------- - def get_recorded_file(self: Amcrest2Mqtt, device_id: str, file: str, encode: bool = True) -> str | None: + async def get_recorded_file(self: Amcrest2Mqtt, device_id: str, file: str, encode: bool = True) -> str | None: + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + device = self.amcrest_devices[device_id] + max_attempts = 3 - tries = 0 - while tries < 3: + for attempt in range(1, max_attempts + 1): try: if self.is_rebooting(device_id): return None + data_raw = cast(bytes, device["camera"].download_file(file)) self.increase_api_calls() if data_raw: @@ -400,7 +438,7 @@ class AmcrestAPIMixin: if len(data_raw) < self.mb_to_b(100): return data_raw.decode("latin-1") else: - self.logger.error(f"skipping raw recording, too large: {self.b_to_mb(len(data_raw))} MB") + self.logger.warning(f"skipping raw recording, too large: {self.b_to_mb(len(data_raw))} MB") return None data_base64 = base64.b64encode(data_raw) self.logger.debug( @@ -411,54 +449,54 @@ class AmcrestAPIMixin: else: self.logger.error(f"skipping recording, too large: {self.b_to_mb(len(data_base64))} MB") return None - except CommError: - tries += 1 - except LoginError: - tries += 1 + except CommError as err: + self.logger.debug(f"failed to get recording from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") + except LoginError as err: + self.logger.debug(f"failed to get recording from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") + except Exception as err: # noqa: BLE001 (log-and-drop is intentional here) + self.logger.debug(f"failed to get recording from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") - self.logger.error(f"failed to get recording from ({self.get_device_name(device_id)})") + self.logger.error(f"failed to get recording from ({self.get_device_name(device_id)}) after {max_attempts} attempts") return None # Events -------------------------------------------------------------------------------------- - async def collect_all_device_events(self: Amcrest2Mqtt) -> None: - tasks = [] - for device_id in self.amcrest_devices: - if self.is_rebooting(device_id): - self.logger.debug(f"skipping collecting events for {self.get_device_name(device_id)}, still rebooting") - continue - tasks.append(self.get_events_from_device(device_id)) - - if tasks: - await asyncio.gather(*tasks) - async def get_events_from_device(self: Amcrest2Mqtt, device_id: str) -> None: device = self.amcrest_devices[device_id] if not device["camera"]: self.logger.warning(f"camera not found for {self.get_device_name(device_id)}") return None + camera = device["camera"] + + max_attempts = 3 - tries = 0 - while tries < 3: + for attempt in range(1, max_attempts + 1): try: if self.is_rebooting(device_id): return None - async for code, payload in device["camera"].async_event_actions("All"): + + async for code, payload in camera.async_event_actions("All"): await self.process_device_event(device_id, code, payload) self.increase_api_calls() return - except CommError: - tries += 1 - except LoginError: - tries += 1 + except CommError as err: + self.logger.debug(f"failed to get events from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") + except LoginError as err: + self.logger.debug(f"failed to get events from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") + except Exception as err: # noqa: BLE001 (log-and-drop is intentional here) + self.logger.debug(f"failed to get events from ({self.get_device_name(device_id)}) on attempt {attempt}: {err!r}") - self.logger.error(f"failed to check for events on ({self.get_device_name(device_id)})") + self.logger.error(f"failed to check for events on ({self.get_device_name(device_id)}) after {max_attempts} attempts ") async def process_device_event(self: Amcrest2Mqtt, device_id: str, code: str, payload: Any) -> None: - try: - device = self.amcrest_devices[device_id] - config = device["config"] + if device_id not in self.amcrest_devices: + self.logger.warning(f"device not found for {device_id}") + return None + + device = self.amcrest_devices[device_id] + config = device["config"] + try: if (code == "ProfileAlarmTransmit" and config["is_ad110"]) or (code == "VideoMotion" and not config["is_ad110"]): motion_payload = {"state": "on" if payload["action"] == "Start" else "off", "region": ", ".join(payload["data"]["RegionName"])} self.events.append({"device_id": device_id, "event": "motion", "payload": motion_payload}) @@ -484,17 +522,11 @@ class AmcrestAPIMixin: self.events.append({"device_id": device_id, "event": "privacy_mode", "payload": "off"}) # lets send these but not bother logging them here - elif code == "TimeChange": - self.events.append({"device_id": device_id, "event": code, "payload": payload["action"]}) - elif code == "NTPAdjustTime": - self.events.append({"device_id": device_id, "event": code, "payload": payload["action"]}) - elif code == "RtspSessionDisconnect": + elif code in ["TimeChange", "NTPAdjustTime", "RtspSessionDisconnect"]: self.events.append({"device_id": device_id, "event": code, "payload": payload["action"]}) - # lets just ignore these - elif code == "InterVideoAccess": # I think this is US, accessing the API of the camera, lets not inception! - pass - elif code == "VideoMotionInfo": + # lets just ignore these completely + elif code in ["InterVideoAccess", "VideoMotionInfo"]: pass # save everything else as a 'generic' event @@ -502,7 +534,7 @@ class AmcrestAPIMixin: self.logger.info(f"logged event on {self.get_device_name(device_id)} - {code}: {payload}") self.events.append({"device_id": device_id, "event": code, "payload": payload}) except Exception as err: - self.logger.error(f"failed to process event from {self.get_device_name(device_id)}: {err}", exc_info=True) + self.logger.error(f"failed to process event from {self.get_device_name(device_id)}: {err!r}") def get_next_event(self: Amcrest2Mqtt) -> dict[str, Any] | None: return self.events.pop(0) if len(self.events) > 0 else None diff --git a/src/amcrest2mqtt/mixins/events.py b/src/amcrest2mqtt/mixins/events.py index f63d2cc..e1b3851 100644 --- a/src/amcrest2mqtt/mixins/events.py +++ b/src/amcrest2mqtt/mixins/events.py @@ -19,35 +19,47 @@ class EventsMixin: device_id = str(device_event["device_id"]) event = str(device_event["event"]) payload = device_event["payload"] - states = self.states[device_id] # if one of our known sensors if event in ["motion", "human", "doorbell", "recording", "privacy_mode", "Reboot"]: if event == "recording": if payload["file"].endswith(".jpg"): - image = self.get_recorded_file(device_id, payload["file"]) + image = await self.get_recorded_file(device_id, payload["file"]) if image: - self.upsert_state( + if self.upsert_state( device_id, camera={"eventshot": image}, sensor={"event_time": datetime.now(timezone.utc).isoformat()}, - ) + ): + needs_publish.add(device_id) event += ": snapshot" elif payload["file"].endswith(".mp4"): - if "path" in self.config["media"] and self.states[device_id]["switch"]["save_recordings"] == "ON": + if "path" in self.config["media"] and self.states[device_id]["switch"].get("save_recordings", "OFF") == "ON": await self.store_recording_in_media(device_id, payload["file"]) event += ": video" elif event == "motion": region = payload["region"] if payload["state"] != "off" else "n/a" - self.upsert_state( + if payload["file"].endswith(".jpg"): + image = await self.get_recorded_file(device_id, payload["file"]) + if image: + if self.upsert_state( + device_id, + camera={"eventshot": image}, + sensor={"event_time": datetime.now(timezone.utc).isoformat()}, + ): + needs_publish.add(device_id) + event += ": snapshot" + elif payload["file"].endswith(".mp4"): + if "path" in self.config["media"] and self.states[device_id]["switch"].get("save_recordings", "OFF") == "ON": + await self.store_recording_in_media(device_id, payload["file"]) + event += ": video" + if self.upsert_state( device_id, binary_sensor={"motion": payload["state"]}, - sensor={ - "motion_region": region, - "event_time": datetime.now(timezone.utc).isoformat(), - }, - ) + sensor={"motion_region": region, "event_time": datetime.now(timezone.utc).isoformat()}, + ): + needs_publish.add(device_id) event += f": ({region}) - {payload["state"]}" else: if isinstance(payload, str): @@ -60,21 +72,22 @@ class EventsMixin: # other ways to infer "privacy mode" has been turned off and we need to update if event in ["motion", "human", "doorbell"] and states["switch"]["privacy"] != "OFF": - self.upsert_state(device_id, switch={"privacy_mode": "OFF"}) + if self.upsert_state(device_id, switch={"privacy_mode": "OFF"}): + needs_publish.add(device_id) # record just these "events": text and time - self.upsert_state( + if self.upsert_state( device_id, sensor={ "event_text": event, "event_time": datetime.now(timezone.utc).isoformat(), }, - ) + ): + needs_publish.add(device_id) self.logger.debug(f'processed event for "{self.get_device_name(device_id)}": {event} with {payload}') - needs_publish.add(device_id) else: self.logger.debug(f'ignored event for "{self.get_device_name(device_id)}": {event} with {payload}') - tasks = [self.publish_device_state(id) for id in needs_publish] + tasks = [self.publish_device_state(device_id) for device_id in needs_publish] if tasks: await asyncio.gather(*tasks) diff --git a/src/amcrest2mqtt/mixins/helpers.py b/src/amcrest2mqtt/mixins/helpers.py index a0ddd41..cb9546f 100644 --- a/src/amcrest2mqtt/mixins/helpers.py +++ b/src/amcrest2mqtt/mixins/helpers.py @@ -250,7 +250,7 @@ class HelpersMixin: return config async def store_recording_in_media(self: Amcrest2Mqtt, device_id: str, amcrest_file: str) -> str | None: - recording = self.get_recorded_file(device_id, amcrest_file, encode=False) + recording = await self.get_recorded_file(device_id, amcrest_file, encode=False) if recording: name = self.get_device_name_slug(device_id) time = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -266,7 +266,7 @@ class HelpersMixin: try: file_path.write_bytes(recording.encode("latin-1")) except IOError as err: - self.logger.error(f"failed to save recording to {file_path}: {err}") + self.logger.error(f"failed to save recording to {file_path}: {err!r}") return None self.upsert_state( @@ -282,7 +282,7 @@ class HelpersMixin: latest_link.unlink() latest_link.symlink_to(local_file) except IOError as err: - self.logger.error(f"failed to save symlink {latest_link} -> {local_file}: {err}") + self.logger.error(f"failed to save symlink {latest_link} -> {local_file}: {err!r}") pass if "media_source" in self.config["media"]: diff --git a/src/amcrest2mqtt/mixins/loops.py b/src/amcrest2mqtt/mixins/loops.py index fabb39f..c82a15a 100644 --- a/src/amcrest2mqtt/mixins/loops.py +++ b/src/amcrest2mqtt/mixins/loops.py @@ -79,7 +79,7 @@ class LoopsMixin: except asyncio.CancelledError: self.logger.warning("main loop cancelled — shutting down...") except Exception as err: - self.logger.exception(f"unhandled exception in main loop: {err}") + self.logger.exception(f"unhandled exception in main loop: {err!r}") self.running = False finally: self.logger.info("all loops terminated — cleanup complete.") diff --git a/src/amcrest2mqtt/mixins/mqtt.py b/src/amcrest2mqtt/mixins/mqtt.py index 347dcb9..240806b 100644 --- a/src/amcrest2mqtt/mixins/mqtt.py +++ b/src/amcrest2mqtt/mixins/mqtt.py @@ -83,12 +83,12 @@ class MqttMixin(BaseMqttMixin): return [vendor, device_id, attribute] except Exception as err: - self.logger.warning(f"malformed device topic with {components}: {err}") + self.logger.warning(f"malformed device topic with {components}: {err!r}") return [] def safe_split_device(self: Amcrest2Mqtt, topic: str, segment: str) -> list[str]: try: return segment.split("-", 1) except ValueError as err: - self.logger.warning(f"ignoring malformed topic {topic}: {err}") + self.logger.warning(f"ignoring malformed topic {topic}: {err!r}") return [] diff --git a/src/amcrest2mqtt/mixins/refresh.py b/src/amcrest2mqtt/mixins/refresh.py index df13772..9dbe49e 100644 --- a/src/amcrest2mqtt/mixins/refresh.py +++ b/src/amcrest2mqtt/mixins/refresh.py @@ -27,3 +27,26 @@ class RefreshMixin: tasks.append(_refresh(device_id)) if tasks: await asyncio.gather(*tasks) + + async def collect_all_device_events(self: Amcrest2Mqtt) -> None: + tasks = [] + for device_id in self.amcrest_devices: + if self.is_rebooting(device_id): + self.logger.debug(f"skipping collecting events for {self.get_device_name(device_id)}, still rebooting") + continue + + tasks.append(self.get_events_from_device(device_id)) + + if tasks: + await asyncio.gather(*tasks) + + async def collect_all_device_snapshots(self: Amcrest2Mqtt) -> None: + tasks = [] + for device_id in self.amcrest_devices: + if self.is_rebooting(device_id): + self.logger.debug(f"skipping snapshot for {self.get_device_name(device_id)}, still rebooting") + continue + tasks.append(self.get_snapshot_from_device(device_id)) + + if tasks: + await asyncio.gather(*tasks) diff --git a/uv.lock b/uv.lock index 3152196..86c8535 100644 --- a/uv.lock +++ b/uv.lock @@ -372,7 +372,7 @@ wheels = [ [[package]] name = "json-logging-graystorm" version = "0.1.3" -source = { git = "https://github.com/weirdtangent/json_logging.git?branch=main#68eaa4b57c9126154e297618c7722e97b00ee41f" } +source = { git = "https://github.com/weirdtangent/json_logging.git?branch=main#5e245601ad2da7676a52281345e4040bb0281ffb" } [[package]] name = "jsonschema" @@ -649,83 +649,83 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.28.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, - { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, - { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, - { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, - { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, - { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, - { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, - { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, - { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, - { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, - { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, - { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, - { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, - { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, - { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, - { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, - { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, - { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, - { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, - { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, - { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, ] [[package]]