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.
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/ci.yml +3 -3
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/docs.yml +3 -3
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.github/workflows/publish.yml +3 -3
- pyclickplc-0.3.2/CLAUDE.md → pyclickplc-0.3.3/AGENTS.md +2 -2
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/CHANGELOG.md +7 -0
- pyclickplc-0.3.3/CLAUDE.md +1 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/PKG-INFO +2 -2
- pyclickplc-0.3.2/docs/CLAUDE.md → pyclickplc-0.3.3/docs/AGENTS.md +1 -1
- pyclickplc-0.3.3/docs/CLAUDE.md +1 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/mkdocs.yml +4 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/pyproject.toml +5 -2
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/blocks.py +44 -4
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/nicknames.py +13 -4
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_blocks.py +75 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_modbus_service.py +21 -4
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/uv.lock +48 -44
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.claude/settings.local.json +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/.gitignore +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/LICENSE +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/Makefile +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/README.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/devtools/lint.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/gen_reference.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/getting-started/quickstart.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/addressing.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/client.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/examples.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/files.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/modbus_service.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/server.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/guides/types.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/index.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/releases/v0.2.0.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/docs/releases/v0.3.0.md +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/sync_clickplc_datetime.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light_dataview.cdv +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/examples/traffic_light_nicknames.csv +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/__init__.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/addresses.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/banks.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/client.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/dataview.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/modbus.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/modbus_service.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/plcdata.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/py.typed +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/server.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/server_tui.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/src/pyclickplc/validation.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/__init__.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/__init__.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/_traffic_light_loader.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_sync_clickplc_datetime.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_cycle.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_files.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/examples/test_traffic_light_smoke.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_addresses.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_banks.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_client.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_dataview.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_integration.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_modbus.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_nicknames.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_plcdata.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_server.py +0 -0
- {pyclickplc-0.3.2 → pyclickplc-0.3.3}/tests/test_server_tui.py +0 -0
- {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@
|
|
40
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
41
41
|
with:
|
|
42
42
|
# Update this as needed:
|
|
43
|
-
version: "0.
|
|
44
|
-
enable-cache:
|
|
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@
|
|
25
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
26
26
|
with:
|
|
27
|
-
version: "0.
|
|
28
|
-
enable-cache:
|
|
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@
|
|
22
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
23
23
|
with:
|
|
24
|
-
version: "0.
|
|
25
|
-
enable-cache:
|
|
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,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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pyclickplc
|
|
3
|
-
Version: 0.3.
|
|
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/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -47,8 +47,8 @@ dependencies = [
|
|
|
47
47
|
dev = [
|
|
48
48
|
"pytest>=8.3.5",
|
|
49
49
|
"pytest-asyncio>=1.0",
|
|
50
|
-
"ruff>=0.
|
|
51
|
-
"ty>=0.0.
|
|
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
|
-
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
187
|
+
value = super().pop(typed_key)
|
|
179
188
|
else:
|
|
180
|
-
value = super().pop(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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.
|
|
611
|
-
{ name = "ty", specifier = ">=0.0.
|
|
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.
|
|
778
|
-
source = { registry = "https://pypi.org/simple" }
|
|
779
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
780
|
-
wheels = [
|
|
781
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
782
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
783
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
784
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
785
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
786
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
787
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
788
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
789
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
790
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
791
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
792
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
793
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
794
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
795
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
796
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
797
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
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.
|
|
821
|
-
source = { registry = "https://pypi.org/simple" }
|
|
822
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
823
|
-
wheels = [
|
|
824
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
825
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
826
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
827
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
828
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
829
|
-
{ url = "https://files.pythonhosted.org/packages/3e/
|
|
830
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
831
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
832
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
833
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
834
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
835
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
836
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
837
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
838
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
839
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|