pyclickplc 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/ci.yml +3 -3
  2. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/docs.yml +3 -3
  3. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/publish.yml +3 -3
  4. pyclickplc-0.3.2/CLAUDE.md → pyclickplc-0.3.3/AGENTS.md +2 -2
  5. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/CHANGELOG.md +7 -0
  6. pyclickplc-0.3.3/CLAUDE.md +1 -0
  7. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/PKG-INFO +2 -2
  8. pyclickplc-0.3.2/docs/CLAUDE.md → pyclickplc-0.3.3/docs/AGENTS.md +1 -1
  9. pyclickplc-0.3.3/docs/CLAUDE.md +1 -0
  10. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/mkdocs.yml +4 -0
  11. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/pyproject.toml +5 -2
  12. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/blocks.py +44 -4
  13. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/nicknames.py +13 -4
  14. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_blocks.py +75 -0
  15. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_modbus_service.py +21 -4
  16. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/uv.lock +48 -44
  17. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.claude/settings.local.json +0 -0
  18. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.gitignore +0 -0
  19. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/LICENSE +0 -0
  20. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/Makefile +0 -0
  21. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/README.md +0 -0
  22. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/devtools/lint.py +0 -0
  23. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/gen_reference.py +0 -0
  24. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/getting-started/quickstart.md +0 -0
  25. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/addressing.md +0 -0
  26. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/client.md +0 -0
  27. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/examples.md +0 -0
  28. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/files.md +0 -0
  29. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/modbus_service.md +0 -0
  30. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/server.md +0 -0
  31. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/types.md +0 -0
  32. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/index.md +0 -0
  33. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/releases/v0.2.0.md +0 -0
  34. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/releases/v0.3.0.md +0 -0
  35. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/sync_clickplc_datetime.py +0 -0
  36. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light.py +0 -0
  37. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light_dataview.cdv +0 -0
  38. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light_nicknames.csv +0 -0
  39. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/__init__.py +0 -0
  40. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/addresses.py +0 -0
  41. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/banks.py +0 -0
  42. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/client.py +0 -0
  43. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/dataview.py +0 -0
  44. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/modbus.py +0 -0
  45. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/modbus_service.py +0 -0
  46. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/plcdata.py +0 -0
  47. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/py.typed +0 -0
  48. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/server.py +0 -0
  49. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/server_tui.py +0 -0
  50. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/validation.py +0 -0
  51. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/__init__.py +0 -0
  52. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/__init__.py +0 -0
  53. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/_traffic_light_loader.py +0 -0
  54. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_sync_clickplc_datetime.py +0 -0
  55. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_cycle.py +0 -0
  56. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_files.py +0 -0
  57. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_smoke.py +0 -0
  58. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_addresses.py +0 -0
  59. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_banks.py +0 -0
  60. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_client.py +0 -0
  61. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_dataview.py +0 -0
  62. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_integration.py +0 -0
  63. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_modbus.py +0 -0
  64. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_nicknames.py +0 -0
  65. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_plcdata.py +0 -0
  66. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_server.py +0 -0
  67. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_server_tui.py +0 -0
  68. {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_validation.py +0 -0
@@ -37,11 +37,11 @@ jobs:
37
37
  fetch-depth: 0
38
38
 
39
39
  - name: Install uv (official Astral action)
40
- uses: astral-sh/setup-uv@v7
40
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
41
41
  with:
42
42
  # Update this as needed:
43
- version: "0.9.25"
44
- enable-cache: true
43
+ version: "0.11.29"
44
+ enable-cache: auto
45
45
  python-version: ${{ matrix.python-version }}
46
46
 
47
47
  - name: Set up Python (using uv)
@@ -22,10 +22,10 @@ jobs:
22
22
  uses: actions/checkout@v6
23
23
 
24
24
  - name: Install uv
25
- uses: astral-sh/setup-uv@v7
25
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
26
26
  with:
27
- version: "0.9.25"
28
- enable-cache: true
27
+ version: "0.11.29"
28
+ enable-cache: auto
29
29
  python-version: "3.12"
30
30
 
31
31
  - name: Set up Python
@@ -19,10 +19,10 @@ jobs:
19
19
  fetch-depth: 0
20
20
 
21
21
  - name: Install uv (official Astral action)
22
- uses: astral-sh/setup-uv@v7
22
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
23
23
  with:
24
- version: "0.9.25"
25
- enable-cache: true
24
+ version: "0.11.29"
25
+ enable-cache: auto
26
26
  python-version: "3.12"
27
27
 
28
28
  - name: Set up Python (using uv)
@@ -1,6 +1,6 @@
1
- # CLAUDE.md
1
+ # pyclickplc Agent Guidance
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3
+ This file provides guidance to coding agents working in this repository.
4
4
 
5
5
  ## Project Overview
6
6
 
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - `compose_structured_block_name()` for building structured block name strings from components (inverse of `parse_structured_block_name()`).
8
+ - `parse_structured_block_name()` now recognizes `Base.Field:udt` (with explicit `:udt` suffix) and `Base:block` (without start override) in addition to existing patterns.
9
+
3
10
  ## 0.3.1 - 2026-03-29
4
11
 
5
12
  ### Fixed
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pyclickplc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Utilities for AutomationDirect CLICK PLCs: Modbus TCP client/server, address helpers, nickname CSV I/O, and DataView CDV I/O.
5
5
  Project-URL: Repository, https://github.com/ssweber/pyclickplc
6
6
  Project-URL: Documentation, https://ssweber.github.io/pyclickplc/
@@ -1,4 +1,4 @@
1
- # pyclickplc Documentation — Context for Claude
1
+ # pyclickplc Documentation — Agent Context
2
2
 
3
3
  ## What is pyclickplc
4
4
 
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -30,6 +30,10 @@ nav:
30
30
  - Releases:
31
31
  - v0.2.0: releases/v0.2.0.md
32
32
 
33
+ not_in_nav: |
34
+ AGENTS.md
35
+ CLAUDE.md
36
+
33
37
  plugins:
34
38
  - search
35
39
  - gen-files:
@@ -47,8 +47,8 @@ dependencies = [
47
47
  dev = [
48
48
  "pytest>=8.3.5",
49
49
  "pytest-asyncio>=1.0",
50
- "ruff>=0.11.0",
51
- "ty>=0.0.14",
50
+ "ruff>=0.16.0",
51
+ "ty>=0.0.65",
52
52
  ]
53
53
  docs = [
54
54
  "mkdocs>=1.6.1,<2",
@@ -87,13 +87,16 @@ bump = true
87
87
  packages = ["src/pyclickplc"]
88
88
 
89
89
  [tool.uv]
90
+ required-version = ">=0.11.29"
90
91
  exclude-newer = "7 days"
92
+ exclude-newer-package = { ty = "0 days" }
91
93
 
92
94
  # ---- Settings ----
93
95
 
94
96
  [tool.ruff]
95
97
  # Set as desired, typically 88 (black standard) or 100 (wide).
96
98
  line-length = 100
99
+ extend-exclude = ["*.md"]
97
100
 
98
101
  [tool.ruff.lint]
99
102
  select = [
@@ -60,10 +60,9 @@ class StructuredBlockName:
60
60
  `parse_block_tag()` treats block names as opaque strings. This helper is
61
61
  intentionally separate and optional for callers that want to interpret
62
62
  naming conventions such as:
63
- - UDT fields: ``Base.field``
63
+ - Semantic blocks: ``Base:block`` / ``Base:block(start=n)``
64
+ - UDT fields: ``Base.field:udt`` (or bare ``Base.field``)
64
65
  - Named arrays: ``Base:named_array(count,stride)``
65
- - Plain blocks with logical start override:
66
- ``Base:block(n)`` / ``Base:block(start=n)``
67
66
  """
68
67
 
69
68
  raw: str
@@ -76,13 +75,16 @@ class StructuredBlockName:
76
75
 
77
76
 
78
77
  _STRUCTURED_IDENT = r"[A-Za-z_][A-Za-z0-9_]*"
79
- _UDT_BLOCK_NAME_RE = re.compile(rf"^(?P<base>{_STRUCTURED_IDENT})\.(?P<field>{_STRUCTURED_IDENT})$")
78
+ _UDT_BLOCK_NAME_RE = re.compile(
79
+ rf"^(?P<base>{_STRUCTURED_IDENT})\.(?P<field>{_STRUCTURED_IDENT})(?::udt)?$"
80
+ )
80
81
  _NAMED_ARRAY_BLOCK_NAME_RE = re.compile(
81
82
  rf"^(?P<base>{_STRUCTURED_IDENT}):named_array\((?P<count>[1-9][0-9]*),(?P<stride>[1-9][0-9]*)\)$"
82
83
  )
83
84
  _BLOCK_START_BLOCK_NAME_RE = re.compile(
84
85
  rf"^(?P<base>{_STRUCTURED_IDENT}):block\((?P<start>(?:0|[1-9][0-9]*|start=(?:0|[1-9][0-9]*)))\)$"
85
86
  )
87
+ _BLOCK_PLAIN_RE = re.compile(rf"^(?P<base>{_STRUCTURED_IDENT}):block$")
86
88
 
87
89
 
88
90
  def parse_structured_block_name(name: str) -> StructuredBlockName:
@@ -124,9 +126,47 @@ def parse_structured_block_name(name: str) -> StructuredBlockName:
124
126
  start=start,
125
127
  )
126
128
 
129
+ block_plain_match = _BLOCK_PLAIN_RE.fullmatch(name)
130
+ if block_plain_match is not None:
131
+ return StructuredBlockName(
132
+ raw=name,
133
+ kind="block",
134
+ base=block_plain_match.group("base"),
135
+ )
136
+
127
137
  return StructuredBlockName(raw=name, kind="plain", base=name)
128
138
 
129
139
 
140
+ def compose_structured_block_name(
141
+ base: str,
142
+ kind: Literal["plain", "udt", "named_array", "block"] = "plain",
143
+ *,
144
+ field: str | None = None,
145
+ count: int | None = None,
146
+ stride: int | None = None,
147
+ start: int | None = None,
148
+ ) -> str:
149
+ """Compose a structured block name string from its components.
150
+
151
+ Inverse of ``parse_structured_block_name()``.
152
+ """
153
+ if kind == "plain":
154
+ return base
155
+ if kind == "udt":
156
+ if not field:
157
+ raise ValueError("UDT kind requires a field name")
158
+ return f"{base}.{field}:udt"
159
+ if kind == "named_array":
160
+ if count is None or stride is None:
161
+ raise ValueError("Named array requires count and stride")
162
+ return f"{base}:named_array({count},{stride})"
163
+ if kind == "block":
164
+ if start is not None:
165
+ return f"{base}:block(start={start})"
166
+ return f"{base}:block"
167
+ raise ValueError(f"Unknown kind: {kind}")
168
+
169
+
130
170
  def group_udt_block_names(names: Iterable[str]) -> dict[str, tuple[str, ...]]:
131
171
  """Group UDT field block names by base name.
132
172
 
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import csv
10
10
  from collections.abc import Iterable, Mapping
11
11
  from pathlib import Path
12
- from typing import Any, cast
12
+ from typing import Any, TypeVar, cast, overload
13
13
 
14
14
  from .addresses import AddressNormalizerMixin, AddressRecord, get_addr_key, parse_address
15
15
  from .banks import BANKS, DEFAULT_RETENTIVE, MEMORY_TYPE_BASES, MEMORY_TYPE_TO_DATA_TYPE, DataType
@@ -38,6 +38,8 @@ DATA_TYPE_CODE_TO_STR: dict[int, str] = {
38
38
  6: "TEXT",
39
39
  }
40
40
 
41
+ _DefaultT = TypeVar("_DefaultT")
42
+
41
43
 
42
44
  class AddressLookupView(AddressNormalizerMixin):
43
45
  """Address-indexed view over an AddressRecordMap."""
@@ -173,11 +175,18 @@ class AddressRecordMap(dict[int, AddressRecord]):
173
175
  super().clear()
174
176
  self._mark_dirty()
175
177
 
176
- def pop(self, key: int, default: Any = ...):
178
+ @overload
179
+ def pop(self, key: object) -> AddressRecord: ...
180
+
181
+ @overload
182
+ def pop(self, key: object, default: _DefaultT) -> AddressRecord | _DefaultT: ...
183
+
184
+ def pop(self, key: object, default: Any = ...) -> Any:
185
+ typed_key = cast(int, key)
177
186
  if default is ...:
178
- value = super().pop(key)
187
+ value = super().pop(typed_key)
179
188
  else:
180
- value = super().pop(key, default)
189
+ value = super().pop(typed_key, default)
181
190
  self._mark_dirty()
182
191
  return value
183
192
 
@@ -2,9 +2,12 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
+ import pytest
6
+
5
7
  from pyclickplc.blocks import (
6
8
  BlockRange,
7
9
  BlockTag,
10
+ compose_structured_block_name,
8
11
  compute_all_block_ranges,
9
12
  extract_block_name,
10
13
  find_block_range_indices,
@@ -211,7 +214,9 @@ class TestOpaqueStructuredNames:
211
214
  def test_structured_names_round_trip(self):
212
215
  names = [
213
216
  "Base.field",
217
+ "Base.field:udt",
214
218
  "Base:named_array(2,3)",
219
+ "Base:block",
215
220
  "Base:block(5)",
216
221
  "Base:block(start=5)",
217
222
  ]
@@ -239,6 +244,12 @@ class TestStructuredBlockNameHelpers:
239
244
  assert parsed.base == "Alarm"
240
245
  assert parsed.field == "id"
241
246
 
247
+ def test_parse_structured_block_name_udt_with_suffix(self):
248
+ parsed = parse_structured_block_name("Motor.Speed:udt")
249
+ assert parsed.kind == "udt"
250
+ assert parsed.base == "Motor"
251
+ assert parsed.field == "Speed"
252
+
242
253
  def test_parse_structured_block_name_named_array(self):
243
254
  parsed = parse_structured_block_name("AlarmPacked:named_array(2,3)")
244
255
  assert parsed.kind == "named_array"
@@ -252,11 +263,71 @@ class TestStructuredBlockNameHelpers:
252
263
  assert parsed.base == "WordBank"
253
264
  assert parsed.start == 5
254
265
 
266
+ def test_parse_structured_block_name_block_plain(self):
267
+ parsed = parse_structured_block_name("Alarms:block")
268
+ assert parsed.kind == "block"
269
+ assert parsed.base == "Alarms"
270
+ assert parsed.start is None
271
+
255
272
  def test_parse_structured_block_name_plain_fallback(self):
256
273
  parsed = parse_structured_block_name("Alarm.bad-name")
257
274
  assert parsed.kind == "plain"
258
275
  assert parsed.base == "Alarm.bad-name"
259
276
 
277
+ def test_compose_plain(self):
278
+ assert compose_structured_block_name("Motor") == "Motor"
279
+
280
+ def test_compose_udt(self):
281
+ assert compose_structured_block_name("Motor", "udt", field="Speed") == "Motor.Speed:udt"
282
+
283
+ def test_compose_named_array(self):
284
+ name = compose_structured_block_name("Ch", "named_array", count=2, stride=3)
285
+ assert name == "Ch:named_array(2,3)"
286
+
287
+ def test_compose_block_no_start(self):
288
+ assert compose_structured_block_name("Alarms", "block") == "Alarms:block"
289
+
290
+ def test_compose_block_with_start(self):
291
+ name = compose_structured_block_name("Alarms", "block", start=0)
292
+ assert name == "Alarms:block(start=0)"
293
+
294
+ def test_compose_udt_missing_field_raises(self):
295
+ with pytest.raises(ValueError, match="field"):
296
+ compose_structured_block_name("Motor", "udt")
297
+
298
+ def test_compose_named_array_missing_args_raises(self):
299
+ with pytest.raises(ValueError, match="count and stride"):
300
+ compose_structured_block_name("Ch", "named_array", count=2)
301
+
302
+ def test_compose_roundtrip_udt(self):
303
+ composed = compose_structured_block_name("Motor", "udt", field="Speed")
304
+ parsed = parse_structured_block_name(composed)
305
+ assert parsed.kind == "udt"
306
+ assert parsed.base == "Motor"
307
+ assert parsed.field == "Speed"
308
+
309
+ def test_compose_roundtrip_named_array(self):
310
+ composed = compose_structured_block_name("Ch", "named_array", count=2, stride=3)
311
+ parsed = parse_structured_block_name(composed)
312
+ assert parsed.kind == "named_array"
313
+ assert parsed.base == "Ch"
314
+ assert parsed.count == 2
315
+ assert parsed.stride == 3
316
+
317
+ def test_compose_roundtrip_block_start(self):
318
+ composed = compose_structured_block_name("Alarms", "block", start=5)
319
+ parsed = parse_structured_block_name(composed)
320
+ assert parsed.kind == "block"
321
+ assert parsed.base == "Alarms"
322
+ assert parsed.start == 5
323
+
324
+ def test_compose_roundtrip_block_no_start(self):
325
+ composed = compose_structured_block_name("Alarms", "block")
326
+ parsed = parse_structured_block_name(composed)
327
+ assert parsed.kind == "block"
328
+ assert parsed.base == "Alarms"
329
+ assert parsed.start is None
330
+
260
331
  def test_group_udt_block_names(self):
261
332
  grouped = group_udt_block_names(
262
333
  [
@@ -273,6 +344,10 @@ class TestStructuredBlockNameHelpers:
273
344
  "Pump": ("state",),
274
345
  }
275
346
 
347
+ def test_group_udt_block_names_with_suffix(self):
348
+ grouped = group_udt_block_names(["Motor.Speed:udt", "Motor.Running:udt"])
349
+ assert grouped == {"Motor": ("Speed", "Running")}
350
+
276
351
 
277
352
  # Helper dataclass for testing matching functions
278
353
  @dataclass
@@ -157,6 +157,23 @@ def _service_threads() -> list[threading.Thread]:
157
157
  ]
158
158
 
159
159
 
160
+ def _record_state(
161
+ states: list[ConnectionState],
162
+ errors: list[Exception | None],
163
+ state: ConnectionState,
164
+ error: Exception | None,
165
+ ) -> None:
166
+ states.append(state)
167
+ errors.append(error)
168
+
169
+
170
+ def _record_values(
171
+ values: list[ModbusResponse], event: threading.Event, response: ModbusResponse
172
+ ) -> None:
173
+ values.append(response)
174
+ event.set()
175
+
176
+
160
177
  class TestLifecycleState:
161
178
  def test_reconnect_config_validation(self):
162
179
  with pytest.raises(ValueError):
@@ -172,7 +189,7 @@ class TestLifecycleState:
172
189
 
173
190
  state_service = ModbusService(
174
191
  poll_interval_s=0.03,
175
- on_state=lambda s, e: (states.append(s), errors.append(e)),
192
+ on_state=lambda s, e: _record_state(states, errors, s, e),
176
193
  )
177
194
  try:
178
195
  state_service.connect("localhost", 15020)
@@ -197,7 +214,7 @@ class TestLifecycleState:
197
214
  errors: list[Exception | None] = []
198
215
  svc = ModbusService(
199
216
  poll_interval_s=0.03,
200
- on_state=lambda s, e: (states.append(s), errors.append(e)),
217
+ on_state=lambda s, e: _record_state(states, errors, s, e),
201
218
  )
202
219
  try:
203
220
  with pytest.raises(OSError):
@@ -244,7 +261,7 @@ class TestLifecycleState:
244
261
  errors: list[Exception | None] = []
245
262
  svc = ModbusService(
246
263
  poll_interval_s=0.03,
247
- on_state=lambda s, e: (states.append(s), errors.append(e)),
264
+ on_state=lambda s, e: _record_state(states, errors, s, e),
248
265
  )
249
266
  try:
250
267
  svc.connect("localhost", 15020)
@@ -318,7 +335,7 @@ class TestPollConfiguration:
318
335
 
319
336
  callback_service = ModbusService(
320
337
  poll_interval_s=0.03,
321
- on_values=lambda r: (values_seen.append(r), callback_event.set()),
338
+ on_values=lambda r: _record_values(values_seen, callback_event, r),
322
339
  )
323
340
  try:
324
341
  callback_service.connect("localhost", 15020)
@@ -3,9 +3,12 @@ revision = 3
3
3
  requires-python = ">=3.11, <4.0"
4
4
 
5
5
  [options]
6
- exclude-newer = "2026-04-08T12:25:42.0940238Z"
6
+ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
7
7
  exclude-newer-span = "P7D"
8
8
 
9
+ [options.exclude-newer-package]
10
+ ty = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" }
11
+
9
12
  [[package]]
10
13
  name = "babel"
11
14
  version = "2.18.0"
@@ -607,8 +610,8 @@ requires-dist = [{ name = "pymodbus", specifier = ">=3.12,<3.13" }]
607
610
  dev = [
608
611
  { name = "pytest", specifier = ">=8.3.5" },
609
612
  { name = "pytest-asyncio", specifier = ">=1.0" },
610
- { name = "ruff", specifier = ">=0.11.0" },
611
- { name = "ty", specifier = ">=0.0.14" },
613
+ { name = "ruff", specifier = ">=0.16.0" },
614
+ { name = "ty", specifier = ">=0.0.65" },
612
615
  ]
613
616
  docs = [
614
617
  { name = "mkdocs", specifier = ">=1.6.1,<2" },
@@ -774,27 +777,27 @@ wheels = [
774
777
 
775
778
  [[package]]
776
779
  name = "ruff"
777
- version = "0.15.7"
778
- source = { registry = "https://pypi.org/simple" }
779
- sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
780
- wheels = [
781
- { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
782
- { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
783
- { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
784
- { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
785
- { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
786
- { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
787
- { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
788
- { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
789
- { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
790
- { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
791
- { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
792
- { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
793
- { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
794
- { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
795
- { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
796
- { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
797
- { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
780
+ version = "0.16.0"
781
+ source = { registry = "https://pypi.org/simple" }
782
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
783
+ wheels = [
784
+ { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
785
+ { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
786
+ { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
787
+ { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
788
+ { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
789
+ { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
790
+ { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
791
+ { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
792
+ { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
793
+ { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
794
+ { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
795
+ { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
796
+ { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
797
+ { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
798
+ { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
799
+ { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
800
+ { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
798
801
  ]
799
802
 
800
803
  [[package]]
@@ -817,26 +820,27 @@ wheels = [
817
820
 
818
821
  [[package]]
819
822
  name = "ty"
820
- version = "0.0.25"
821
- source = { registry = "https://pypi.org/simple" }
822
- sdist = { url = "https://files.pythonhosted.org/packages/12/bf/3c3147c7237277b0e8a911ff89de7183408be96b31fb42b38edb666d287f/ty-0.0.25.tar.gz", hash = "sha256:8ae3891be17dfb6acab51a2df3a8f8f6c551eb60ea674c10946dc92aae8d4401", size = 5375500, upload-time = "2026-03-24T22:32:34.608Z" }
823
- wheels = [
824
- { url = "https://files.pythonhosted.org/packages/97/a4/6c289cbd1474285223124a4ffb55c078dbe9ae1d925d0b6a948643c7f115/ty-0.0.25-py3-none-linux_armv6l.whl", hash = "sha256:26d6d5aede5d54fb055779460f896d9c1473c6fb996716bd11cb90f027d8fee7", size = 10452747, upload-time = "2026-03-24T22:32:32.662Z" },
825
- { url = "https://files.pythonhosted.org/packages/00/13/74cb9de356b9ceb3f281ab048f8c4ac2207122161b0ac0066886ce129abe/ty-0.0.25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aedcfbc7b6b96dbc55b0da78fa02bd049373ff3d8a827f613dadd8bd17d10758", size = 10271349, upload-time = "2026-03-24T22:32:13.041Z" },
826
- { url = "https://files.pythonhosted.org/packages/0e/93/ffc5a20cc9e14fa9b32b0c54884864bede30d144ce2ae013805bce0c86d0/ty-0.0.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0a8fb3c1e28f73618941811e2568dca195178a1a6314651d4ee97086a4497253", size = 9730308, upload-time = "2026-03-24T22:32:19.24Z" },
827
- { url = "https://files.pythonhosted.org/packages/6d/78/52e05ef32a5f172fce70633a4e19d8e04364271a4322ae12382c7344b0de/ty-0.0.25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814870b7f347b5d0276304cddb98a0958f08de183bf159abc920ebe321247ad4", size = 10247664, upload-time = "2026-03-24T22:32:08.669Z" },
828
- { url = "https://files.pythonhosted.org/packages/c2/64/0d0a47ed0aa1d634c666c2cc15d3b0af4b95d0fd3dbb796032bd493f3433/ty-0.0.25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:781150e23825dc110cd5e1f50ca3d61664f7a5db5b4a55d5dbf7d3b1e246b917", size = 10261961, upload-time = "2026-03-24T22:32:43.935Z" },
829
- { url = "https://files.pythonhosted.org/packages/3e/ba/4666b96f0499465efb97c244554107c541d74a1add393e62276b3de9b54f/ty-0.0.25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc81ff2a0143911321251dc81d1c259fa5cdc56d043019a733c845d55409e2a", size = 10746076, upload-time = "2026-03-24T22:32:26.37Z" },
830
- { url = "https://files.pythonhosted.org/packages/e7/ed/aa958ccbcd85cc206600e48fbf0a1c27aef54b4b90112d9a73f69ed0c739/ty-0.0.25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f03c5c5b5c10355ea030cbe3cd93b2e759b9492c66688288ea03a68086069f2e", size = 11287331, upload-time = "2026-03-24T22:32:21.607Z" },
831
- { url = "https://files.pythonhosted.org/packages/26/e4/f4a004e1952e6042f5bfeeb7d09cffb379270ef009d9f8568471863e86e6/ty-0.0.25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fc1ef49cd6262eb9223ccf6e258ac899aaa53e7dc2151ba65a2c9fa248dfa75", size = 11028804, upload-time = "2026-03-24T22:32:39.088Z" },
832
- { url = "https://files.pythonhosted.org/packages/56/32/5c15bb8ea20ed54d43c734f253a2a5da95d41474caecf4ef3682df9f68f5/ty-0.0.25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad98da1393161096235a387cc36abecd31861060c68416761eccdb7c1bc326b", size = 10845246, upload-time = "2026-03-24T22:32:41.33Z" },
833
- { url = "https://files.pythonhosted.org/packages/6f/fe/4ddd83e810c8682fcfada0d1c9d38936a34a024d32d7736075c1e53a038e/ty-0.0.25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2d4336aa5381eb4eab107c3dec75fe22943a648ef6646f5a8431ef1c8cdabb66", size = 10233515, upload-time = "2026-03-24T22:32:17.012Z" },
834
- { url = "https://files.pythonhosted.org/packages/ad/db/9fe54f6fb952e5b218f2e661e64ed656512edf2046cfbb9c159558e255db/ty-0.0.25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e10ed39564227de2b7bd89398250b65daaedbef15a25cef8eee70078f5d9e0b2", size = 10275289, upload-time = "2026-03-24T22:32:28.21Z" },
835
- { url = "https://files.pythonhosted.org/packages/b1/e0/090d7b33791b42bc7ec29463ac6a634738e16b289e027608ebe542682773/ty-0.0.25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aca04e9ed9b61c706064a1c0b71a247c3f92f373d0222103f3bc54b649421796", size = 10461195, upload-time = "2026-03-24T22:32:24.252Z" },
836
- { url = "https://files.pythonhosted.org/packages/42/31/5bf12bce01b80b72a7a4e627380779b41510e730f6000862a1d078e423f7/ty-0.0.25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:18a5443e4ef339c1bd8c57fc13112c22080617ea582bfc22b497d82d65361325", size = 10931471, upload-time = "2026-03-24T22:32:14.985Z" },
837
- { url = "https://files.pythonhosted.org/packages/6a/5e/ab60c11f8a6dd2a0ae96daac83458ef2e9be1ae70481d1ad9c59d3eaf20f/ty-0.0.25-py3-none-win32.whl", hash = "sha256:a685b9a611b69195b5a557e05dbb7ebcd12815f6c32fb27fdf15edeb1fa33d8f", size = 9835974, upload-time = "2026-03-24T22:32:36.86Z" },
838
- { url = "https://files.pythonhosted.org/packages/41/55/625acc2ef34646268bc2baa8fdd6e22fb47cd5965e2acd3be92c687fb6b0/ty-0.0.25-py3-none-win_amd64.whl", hash = "sha256:0d4d37a1f1ab7f2669c941c38c65144ff223eb51ececd7ccfc0d623afbc0f729", size = 10815449, upload-time = "2026-03-24T22:32:11.031Z" },
839
- { url = "https://files.pythonhosted.org/packages/82/c7/0147bfb543df97740b45b222c54ff79ef20fa57f14b9d2c1dab3cd7d3faa/ty-0.0.25-py3-none-win_arm64.whl", hash = "sha256:d80b8cd965cbacbfd887ac2d985f5b6da09b7aa3569371e2894e0b30b26b89cd", size = 10225494, upload-time = "2026-03-24T22:32:30.611Z" },
823
+ version = "0.0.65"
824
+ source = { registry = "https://pypi.org/simple" }
825
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cf561927e8e9ab5c1892a833b664aa9cd6f051a75f6280c66d8047246bda/ty-0.0.65.tar.gz", hash = "sha256:b7134bffcc00b715fa8291e84d845782ced810a998dc1f7f11d71c85c4046325", size = 6460098, upload-time = "2026-07-29T18:31:03.27Z" }
826
+ wheels = [
827
+ { url = "https://files.pythonhosted.org/packages/7b/4e/71e2d325d2b53a1afad81624ad076b2ede413213fc4a18cb05b78c568571/ty-0.0.65-py3-none-linux_armv6l.whl", hash = "sha256:dc556c9f05408bef4c4ef02b2cc382e4e5f797b4b20d64410289848f0d76705f", size = 12298466, upload-time = "2026-07-29T18:30:12.744Z" },
828
+ { url = "https://files.pythonhosted.org/packages/57/77/fec8f29647c55794efa430a7f365e44f5ce7ffb6459d9445a87fac569bec/ty-0.0.65-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d2e0d34cc0a28a17ef0cf81135c5ebabc3562131f9079138ba5e7bae0f56bd", size = 11942421, upload-time = "2026-07-29T18:30:16.076Z" },
829
+ { url = "https://files.pythonhosted.org/packages/13/09/7f3766aef9dc627e2698cf4e3e59cf53389dcae3812040d33c1aa931230f/ty-0.0.65-py3-none-macosx_11_0_arm64.whl", hash = "sha256:685f49a9312bbf69d5b65bbb66384fed1f927403ea030c217b9289092d7e46c4", size = 11451922, upload-time = "2026-07-29T18:30:19.155Z" },
830
+ { url = "https://files.pythonhosted.org/packages/cb/7b/1a77cd50e0befb50f55b8bf9bd3ed3eddf184bf28c61b56727039e0774fc/ty-0.0.65-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f564b5ebe78e2f3a8e7b8eacb1292eb88b7c0f3c8630671cfca31abc0709cd9", size = 11994999, upload-time = "2026-07-29T18:30:22.315Z" },
831
+ { url = "https://files.pythonhosted.org/packages/63/7b/feda16f3a4a0a99be27431e0c9598eeeec0db1eb2fec9a15976698209418/ty-0.0.65-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c983e156fe9e113fb56389e13d327b6b8549fe866de9b269684723a88e9b732d", size = 12090662, upload-time = "2026-07-29T18:30:24.93Z" },
832
+ { url = "https://files.pythonhosted.org/packages/ed/3e/3f69bf9c9307dbdc0771719f65ce5b556e7bdeeaccbdd599d4f57866d801/ty-0.0.65-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3663b7396e8b1a9954e20e732de7ccb0192bf4118473069b4945920d6923921", size = 12822094, upload-time = "2026-07-29T18:30:28.012Z" },
833
+ { url = "https://files.pythonhosted.org/packages/90/38/8fa791b3bb503ee2b46ad81690cd1bdd54519582df6d805cee57fe143e85/ty-0.0.65-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306ed01f29d6e108e98feb233dbbf5878a027603b71bd3743b343977933a9f16", size = 13357833, upload-time = "2026-07-29T18:30:31.122Z" },
834
+ { url = "https://files.pythonhosted.org/packages/c1/73/4dda396a201e1dd0ed3594a9b48e559cb41c4bc048c6cd4c4d1b39eb4313/ty-0.0.65-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28bcfc8898c94f079a9100e684bcf312b6a64ad3a7d4ebb35a4591546030a2cd", size = 12977303, upload-time = "2026-07-29T18:30:33.944Z" },
835
+ { url = "https://files.pythonhosted.org/packages/a5/26/c250c2c569adc53a8591716641388397bcb2a442e4a30b952ae81b50c0e0/ty-0.0.65-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a75bd0c245c38802a8f488378e74f92feb7dd33db7d63fbdd6fdf82791ba730", size = 12579338, upload-time = "2026-07-29T18:30:37.199Z" },
836
+ { url = "https://files.pythonhosted.org/packages/d3/94/4a5647d44753ca218fc930d7e4d9bf468d0ed4a0ad4b3d57588bc1bbacf7/ty-0.0.65-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9e5e1bdea9662d2b5312b4e99f319f4e6e2ea427511b5fbc546141b79ec53f76", size = 12957731, upload-time = "2026-07-29T18:30:39.937Z" },
837
+ { url = "https://files.pythonhosted.org/packages/36/b6/1e22fa11a1e0dfb20b1c7f3cbfd8170273aada2a82f9ecd3055275370c44/ty-0.0.65-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:03a88493d4842889f65280ae241e06b399d57eb3c63571054cad21a4c33b3b69", size = 11938625, upload-time = "2026-07-29T18:30:42.603Z" },
838
+ { url = "https://files.pythonhosted.org/packages/5c/0a/fe5f22ef62b193201bc5566762e22049762cd485bfafb5095a7050760054/ty-0.0.65-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:600b8bf6f4940cf7ffb2f43d3716faaf38dcb97cd8617c55771451bc0276408f", size = 12105592, upload-time = "2026-07-29T18:30:45.419Z" },
839
+ { url = "https://files.pythonhosted.org/packages/76/fd/922b3a6e9d697452cdbb4b7e3f636868add5ec652154518a736e4364f3b7/ty-0.0.65-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c28007bc79d648c1ddaf1e65885d07baec48eb87240da442f608e4107c1b7d8", size = 12387335, upload-time = "2026-07-29T18:30:48.405Z" },
840
+ { url = "https://files.pythonhosted.org/packages/77/22/a1a08ebc84c083db2fb55e3b5cd186db0c067692f4921146f601360231e2/ty-0.0.65-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c852da96091ad22361e6586b7c7ba98e1334dcd4d8ffb67e47f4fb673de33f77", size = 12682710, upload-time = "2026-07-29T18:30:51.364Z" },
841
+ { url = "https://files.pythonhosted.org/packages/81/14/eaaa410a25bbdea19722109b5422380a0e211b3afcf3071d15953ddbd5db/ty-0.0.65-py3-none-win32.whl", hash = "sha256:cf529d538f1403b14b0511e6ec3cdb95d3d974adabf24cc76cedc533368c3edc", size = 11692341, upload-time = "2026-07-29T18:30:54.35Z" },
842
+ { url = "https://files.pythonhosted.org/packages/bc/0f/6d48f206dce9d7e53fe3b5ea0f0ab5800dd9d2365b2b48f736783436c43f/ty-0.0.65-py3-none-win_amd64.whl", hash = "sha256:234a321e33c7cbbfbd67bfa0b01b685dd9c21f1841781a21e5ca1fa0b25f1d5d", size = 12729355, upload-time = "2026-07-29T18:30:57.275Z" },
843
+ { url = "https://files.pythonhosted.org/packages/96/aa/7446f7725e303cf78e058c893af1f0552b9451895454908706f4c6c3494b/ty-0.0.65-py3-none-win_arm64.whl", hash = "sha256:b9424be1ec56d93ff18609fb1c0a0a2283fe1282cd6d1c7604f97d73b94d61f2", size = 12051375, upload-time = "2026-07-29T18:31:00.579Z" },
840
844
  ]
841
845
 
842
846
  [[package]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes