python-duco-connectivity 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,49 @@
1
+ """Public package exports for python-duco-connectivity."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from .client import DucoClient
6
+ from .exceptions import DucoConnectionError, DucoError, DucoWriteLimitError
7
+ from .models import (
8
+ ApiEndpoint,
9
+ ApiInfo,
10
+ BoardInfo,
11
+ DiagComponent,
12
+ DiagStatus,
13
+ LanInfo,
14
+ NetworkType,
15
+ Node,
16
+ NodeGeneralInfo,
17
+ NodeSensorInfo,
18
+ NodeType,
19
+ NodeVentilationInfo,
20
+ VentilationMode,
21
+ VentilationState,
22
+ )
23
+
24
+ try:
25
+ __version__ = version("python-duco-connectivity")
26
+ except PackageNotFoundError:
27
+ __version__ = "0.0.0"
28
+
29
+ __all__ = [
30
+ "ApiEndpoint",
31
+ "ApiInfo",
32
+ "BoardInfo",
33
+ "DucoClient",
34
+ "DucoConnectionError",
35
+ "DucoError",
36
+ "DucoWriteLimitError",
37
+ "DiagComponent",
38
+ "DiagStatus",
39
+ "LanInfo",
40
+ "NetworkType",
41
+ "Node",
42
+ "NodeGeneralInfo",
43
+ "NodeSensorInfo",
44
+ "NodeType",
45
+ "NodeVentilationInfo",
46
+ "VentilationMode",
47
+ "VentilationState",
48
+ "__version__",
49
+ ]
@@ -0,0 +1,251 @@
1
+ """Async client for the local Duco HTTP API."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import aiohttp
7
+
8
+ from .exceptions import DucoConnectionError, DucoError, DucoWriteLimitError
9
+ from .models import (
10
+ ApiEndpoint,
11
+ ApiInfo,
12
+ BoardInfo,
13
+ DiagComponent,
14
+ DiagStatus,
15
+ LanInfo,
16
+ NetworkType,
17
+ Node,
18
+ NodeGeneralInfo,
19
+ NodeSensorInfo,
20
+ NodeType,
21
+ NodeVentilationInfo,
22
+ VentilationMode,
23
+ VentilationState,
24
+ )
25
+
26
+
27
+ class DucoClient:
28
+ """Client for a Duco box that exposes the local HTTP API."""
29
+
30
+ def __init__(
31
+ self,
32
+ session: aiohttp.ClientSession,
33
+ host: str,
34
+ *,
35
+ port: int | None = None,
36
+ request_timeout: float = 10.0,
37
+ ) -> None:
38
+ self._session = session
39
+ self._timeout = aiohttp.ClientTimeout(total=request_timeout)
40
+ if host.startswith("https://"):
41
+ msg = "HTTPS is not supported by this client"
42
+ raise ValueError(msg)
43
+ normalized_host = host.removeprefix("http://").removeprefix("https://").rstrip("/")
44
+ if "://" in host and not host.startswith("http://"):
45
+ msg = f"Unsupported scheme in host value: {host}"
46
+ raise ValueError(msg)
47
+ if port is None:
48
+ self._base_url = f"http://{normalized_host}"
49
+ else:
50
+ self._base_url = f"http://{normalized_host}:{port}"
51
+
52
+ @property
53
+ def base_url(self) -> str:
54
+ """Normalized base URL used for requests."""
55
+ return self._base_url
56
+
57
+ async def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
58
+ if "json" in kwargs:
59
+ payload = kwargs.pop("json")
60
+ kwargs["data"] = json.dumps(payload, separators=(",", ":")).encode()
61
+ kwargs.setdefault("headers", {})["Content-Type"] = "application/json"
62
+ kwargs.setdefault("timeout", self._timeout)
63
+
64
+ try:
65
+ request = self._session.request(method, f"{self._base_url}{path}", **kwargs)
66
+ except (aiohttp.ClientError, TimeoutError) as err:
67
+ msg = f"Could not reach Duco device at {self._base_url}: {err}"
68
+ raise DucoConnectionError(msg) from err
69
+
70
+ try:
71
+ async with request as response:
72
+ if response.status == 429:
73
+ raise DucoWriteLimitError()
74
+
75
+ if response.status >= 400:
76
+ body = await response.text()
77
+ msg = f"Unexpected response {response.status} for {path}: {body}"
78
+ raise DucoError(msg)
79
+
80
+ try:
81
+ return await response.json(content_type=None)
82
+ except ValueError as err:
83
+ msg = f"Expected JSON response from {path}: {err}"
84
+ raise DucoError(msg) from err
85
+ except DucoError:
86
+ raise
87
+ except (aiohttp.ClientError, TimeoutError) as err:
88
+ msg = f"Could not reach Duco device at {self._base_url}: {err}"
89
+ raise DucoConnectionError(msg) from err
90
+
91
+ @staticmethod
92
+ def _read_wrapped_value(payload: dict[str, Any], key: str) -> Any:
93
+ return payload[key]["Val"]
94
+
95
+ @staticmethod
96
+ def _to_node_type(raw_value: str) -> NodeType:
97
+ try:
98
+ return NodeType(raw_value)
99
+ except ValueError:
100
+ return NodeType.UNKNOWN
101
+
102
+ @staticmethod
103
+ def _to_network_type(raw_value: str) -> NetworkType:
104
+ try:
105
+ return NetworkType(raw_value)
106
+ except ValueError:
107
+ return NetworkType.UNKNOWN
108
+
109
+ async def async_get_api_info(self) -> ApiInfo:
110
+ """Return API metadata advertised by the box."""
111
+ payload = await self._request_json("GET", "/api")
112
+ public_api_version = self._read_wrapped_value(payload, "PublicApiVersion")
113
+ reported_api_version = None
114
+ if "ApiVersion" in payload:
115
+ reported_api_version = self._read_wrapped_value(payload, "ApiVersion")
116
+ endpoints = [
117
+ ApiEndpoint(
118
+ url=item["Url"],
119
+ methods=list(item.get("Methods", [])),
120
+ query_parameters=list(item.get("QueryParameters", [])),
121
+ modules=list(item.get("Modules", [])),
122
+ )
123
+ for item in payload.get("ApiInfo", [])
124
+ ]
125
+ return ApiInfo(
126
+ public_api_version=public_api_version,
127
+ reported_api_version=reported_api_version,
128
+ endpoints=endpoints,
129
+ )
130
+
131
+ async def async_get_board_info(self) -> BoardInfo:
132
+ """Return identity and version details for the main unit."""
133
+ payload = await self._request_json(
134
+ "GET",
135
+ "/info",
136
+ params={"module": "General", "submodule": "Board"},
137
+ )
138
+ board = payload["General"]["Board"]
139
+ return BoardInfo(
140
+ box_name=self._read_wrapped_value(board, "BoxName"),
141
+ box_sub_type_name=self._read_wrapped_value(board, "BoxSubTypeName"),
142
+ serial_board_box=self._read_wrapped_value(board, "SerialBoardBox"),
143
+ serial_board_comm=self._read_wrapped_value(board, "SerialBoardComm"),
144
+ serial_duco_box=self._read_wrapped_value(board, "SerialDucoBox"),
145
+ serial_duco_comm=self._read_wrapped_value(board, "SerialDucoComm"),
146
+ time=self._read_wrapped_value(board, "Time"),
147
+ public_api_version=self._read_wrapped_value(board, "PublicApiVersion")
148
+ if "PublicApiVersion" in board
149
+ else None,
150
+ software_version=self._read_wrapped_value(board, "SwVersion")
151
+ if "SwVersion" in board
152
+ else None,
153
+ )
154
+
155
+ async def async_get_lan_info(self) -> LanInfo:
156
+ """Return LAN settings reported by the box."""
157
+ payload = await self._request_json(
158
+ "GET",
159
+ "/info",
160
+ params={"module": "General", "submodule": "Lan"},
161
+ )
162
+ lan = payload["General"]["Lan"]
163
+ return LanInfo(
164
+ mode=self._read_wrapped_value(lan, "Mode"),
165
+ ip=self._read_wrapped_value(lan, "Ip"),
166
+ net_mask=self._read_wrapped_value(lan, "NetMask"),
167
+ default_gateway=self._read_wrapped_value(lan, "DefaultGateway"),
168
+ dns=self._read_wrapped_value(lan, "Dns"),
169
+ mac=self._read_wrapped_value(lan, "Mac"),
170
+ host_name=self._read_wrapped_value(lan, "HostName"),
171
+ rssi_wifi=self._read_wrapped_value(lan, "RssiWifi") if "RssiWifi" in lan else None,
172
+ )
173
+
174
+ async def async_get_diagnostics(self) -> list[DiagComponent]:
175
+ """Return health states for diagnostic subsystems."""
176
+ payload = await self._request_json("GET", "/info", params={"module": "Diag"})
177
+ return [
178
+ DiagComponent(
179
+ component=item["Component"],
180
+ status=DiagStatus(item["Status"]),
181
+ )
182
+ for item in payload["Diag"]["SubSystems"]
183
+ ]
184
+
185
+ async def async_get_nodes(self) -> list[Node]:
186
+ """Return nodes reported by the local API."""
187
+ payload = await self._request_json("GET", "/info/nodes")
188
+ return [self._parse_node(item) for item in payload["Nodes"]]
189
+
190
+ async def async_get_write_requests_remaining(self) -> int:
191
+ """Return the remaining write budget reported by the box."""
192
+ payload = await self._request_json(
193
+ "GET",
194
+ "/info",
195
+ params={"module": "General", "submodule": "PublicApi"},
196
+ )
197
+ return int(self._read_wrapped_value(payload["General"]["PublicApi"], "WriteReqCntRemain"))
198
+
199
+ async def async_set_ventilation_state(
200
+ self, node_id: int, state: VentilationState | str
201
+ ) -> None:
202
+ """Request a ventilation state change for a node."""
203
+ state_value = state.value if isinstance(state, VentilationState) else state
204
+ await self._request_json(
205
+ "POST",
206
+ f"/action/nodes/{node_id}",
207
+ json={"Action": "SetVentilationState", "Val": state_value},
208
+ )
209
+
210
+ def _parse_node(self, payload: dict[str, Any]) -> Node:
211
+ general = payload["General"]
212
+ node_general = NodeGeneralInfo(
213
+ node_type=self._to_node_type(self._read_wrapped_value(general, "Type")),
214
+ sub_type=self._read_wrapped_value(general, "SubType"),
215
+ network_type=self._to_network_type(self._read_wrapped_value(general, "NetworkType")),
216
+ parent=self._read_wrapped_value(general, "Parent"),
217
+ asso=self._read_wrapped_value(general, "Asso"),
218
+ name=self._read_wrapped_value(general, "Name"),
219
+ identify=self._read_wrapped_value(general, "Identify"),
220
+ )
221
+
222
+ ventilation = None
223
+ if "Ventilation" in payload:
224
+ vent = payload["Ventilation"]
225
+ ventilation = NodeVentilationInfo(
226
+ state=VentilationState(self._read_wrapped_value(vent, "State")),
227
+ mode=VentilationMode(self._read_wrapped_value(vent, "Mode")),
228
+ time_state_remain=self._read_wrapped_value(vent, "TimeStateRemain"),
229
+ time_state_end=self._read_wrapped_value(vent, "TimeStateEnd"),
230
+ flow_lvl_tgt=self._read_wrapped_value(vent, "FlowLvlTgt")
231
+ if "FlowLvlTgt" in vent
232
+ else None,
233
+ )
234
+
235
+ sensor = None
236
+ if "Sensor" in payload:
237
+ sensor = payload["Sensor"]
238
+ sensor = NodeSensorInfo(
239
+ co2=self._read_wrapped_value(sensor, "Co2") if "Co2" in sensor else None,
240
+ iaq_co2=self._read_wrapped_value(sensor, "IaqCo2") if "IaqCo2" in sensor else None,
241
+ rh=self._read_wrapped_value(sensor, "Rh") if "Rh" in sensor else None,
242
+ iaq_rh=self._read_wrapped_value(sensor, "IaqRh") if "IaqRh" in sensor else None,
243
+ temp=self._read_wrapped_value(sensor, "Temp") if "Temp" in sensor else None,
244
+ )
245
+
246
+ return Node(
247
+ node_id=payload["Node"],
248
+ general=node_general,
249
+ ventilation=ventilation,
250
+ sensor=sensor,
251
+ )
@@ -0,0 +1,20 @@
1
+ """Exceptions raised by the Duco client."""
2
+
3
+
4
+ class DucoError(Exception):
5
+ """Base class for client errors."""
6
+
7
+
8
+ class DucoConnectionError(DucoError):
9
+ """Raised when the client cannot reach the box."""
10
+
11
+
12
+ class DucoWriteLimitError(DucoError):
13
+ """Raised when the box rejects writes because its budget is exhausted."""
14
+
15
+ def __init__(self, remaining: int | None = None) -> None:
16
+ self.remaining = remaining
17
+ detail = "Duco write capacity exhausted"
18
+ if remaining is not None:
19
+ detail = f"{detail} ({remaining} writes remaining)"
20
+ super().__init__(detail)
@@ -0,0 +1,165 @@
1
+ """Typed models for values exposed by the local Duco API."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+
6
+
7
+ class NodeType(StrEnum):
8
+ """Node categories reported by the box."""
9
+
10
+ BOX = "BOX"
11
+ UCCO2 = "UCCO2"
12
+ UCRH = "UCRH"
13
+ UCBAT = "UCBAT"
14
+ UC = "UC"
15
+ BSRH = "BSRH"
16
+ VLV = "VLV"
17
+ UNKNOWN = "UNKNOWN"
18
+
19
+
20
+ class NetworkType(StrEnum):
21
+ """Transport types reported for node connectivity."""
22
+
23
+ VIRT = "VIRT"
24
+ RF = "RF"
25
+ WI = "WI"
26
+ UNKNOWN = "UNKNOWN"
27
+
28
+
29
+ class VentilationMode(StrEnum):
30
+ """Control modes reported for node ventilation."""
31
+
32
+ AUTO = "AUTO"
33
+ MANU = "MANU"
34
+ NONE = "-"
35
+
36
+
37
+ class VentilationState(StrEnum):
38
+ """State values accepted and reported for node ventilation."""
39
+
40
+ AUTO = "AUTO"
41
+ AUT1 = "AUT1"
42
+ AUT2 = "AUT2"
43
+ AUT3 = "AUT3"
44
+ MAN1 = "MAN1"
45
+ MAN2 = "MAN2"
46
+ MAN3 = "MAN3"
47
+ EMPT = "EMPT"
48
+ CNT1 = "CNT1"
49
+ CNT2 = "CNT2"
50
+ CNT3 = "CNT3"
51
+ MAN1x2 = "MAN1x2"
52
+ MAN2x2 = "MAN2x2"
53
+ MAN3x2 = "MAN3x2"
54
+ MAN1x3 = "MAN1x3"
55
+ MAN2x3 = "MAN2x3"
56
+ MAN3x3 = "MAN3x3"
57
+
58
+
59
+ class DiagStatus(StrEnum):
60
+ """Health states returned by the diagnostics API."""
61
+
62
+ OK = "Ok"
63
+ DISABLE = "Disable"
64
+ ERROR = "Error"
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class ApiEndpoint:
69
+ """Capabilities advertised for a single API endpoint."""
70
+
71
+ url: str
72
+ methods: list[str] = field(default_factory=list)
73
+ query_parameters: list[str] = field(default_factory=list)
74
+ modules: list[str] = field(default_factory=list)
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class ApiInfo:
79
+ """Version and endpoint metadata returned by the API root."""
80
+
81
+ public_api_version: str
82
+ reported_api_version: str | None = None
83
+ endpoints: list[ApiEndpoint] = field(default_factory=list)
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class BoardInfo:
88
+ """Identity and version fields for the main Duco unit."""
89
+
90
+ box_name: str
91
+ box_sub_type_name: str
92
+ serial_board_box: str
93
+ serial_board_comm: str
94
+ serial_duco_box: str
95
+ serial_duco_comm: str
96
+ time: int
97
+ public_api_version: str | None = None
98
+ software_version: str | None = None
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class LanInfo:
103
+ """LAN settings reported by the main unit."""
104
+
105
+ mode: str
106
+ ip: str
107
+ net_mask: str
108
+ default_gateway: str
109
+ dns: str
110
+ mac: str
111
+ host_name: str
112
+ rssi_wifi: int | None
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class NodeSensorInfo:
117
+ """Sensor readings reported for a node."""
118
+
119
+ co2: int | None = None
120
+ iaq_co2: int | None = None
121
+ rh: float | None = None
122
+ iaq_rh: int | None = None
123
+ temp: float | None = None
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class NodeVentilationInfo:
128
+ """Ventilation state and timers reported for a node."""
129
+
130
+ state: VentilationState
131
+ mode: VentilationMode
132
+ time_state_remain: int
133
+ time_state_end: int
134
+ flow_lvl_tgt: int | None = None
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class NodeGeneralInfo:
139
+ """Static node metadata used to identify a device."""
140
+
141
+ node_type: NodeType
142
+ sub_type: int
143
+ network_type: NetworkType
144
+ parent: int
145
+ asso: int
146
+ name: str
147
+ identify: int
148
+
149
+
150
+ @dataclass(frozen=True, slots=True)
151
+ class Node:
152
+ """Node returned by the local Duco API."""
153
+
154
+ node_id: int
155
+ general: NodeGeneralInfo
156
+ ventilation: NodeVentilationInfo | None = None
157
+ sensor: NodeSensorInfo | None = None
158
+
159
+
160
+ @dataclass(frozen=True, slots=True)
161
+ class DiagComponent:
162
+ """Health state for a diagnostic subsystem."""
163
+
164
+ component: str
165
+ status: DiagStatus
File without changes
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-duco-connectivity
3
+ Version: 0.1.0
4
+ Summary: Async HTTP client for the local Duco Connectivity API
5
+ Author: Ronald van der Meer
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ronaldvdmeer/python-duco-connectivity
8
+ Project-URL: Repository, https://github.com/ronaldvdmeer/python-duco-connectivity
9
+ Project-URL: Issues, https://github.com/ronaldvdmeer/python-duco-connectivity/issues
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Home Automation
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: aiohttp>=3.9.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: aioresponses>=0.7; extra == "dev"
24
+ Requires-Dist: bandit>=1.7; extra == "dev"
25
+ Requires-Dist: mypy>=1.8; extra == "dev"
26
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
29
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.11; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # python-duco-connectivity
34
+
35
+ Async Python client for the local Duco HTTP API.
36
+
37
+ `python-duco-connectivity` is a small async client for the unauthenticated
38
+ local Duco HTTP endpoints that were validated during initial development. The
39
+ library keeps its public models close to the API payload shape and is intended
40
+ to stay reusable outside Home Assistant.
41
+
42
+ ## Installation
43
+
44
+ Until the first PyPI release is published, install directly from GitHub:
45
+
46
+ ```bash
47
+ pip install git+https://github.com/ronaldvdmeer/python-duco-connectivity.git
48
+ ```
49
+
50
+ After the package is published on PyPI, install it with:
51
+
52
+ ```bash
53
+ pip install python-duco-connectivity
54
+ ```
55
+
56
+ ## Current scope
57
+
58
+ - HTTP only
59
+ - asynchronous communication via `aiohttp`
60
+ - typed models that stay close to the API response shape
61
+
62
+ ## Public API surface
63
+
64
+ The current client exposes:
65
+
66
+ - `async_get_api_info()` for `GET /api`
67
+ - `async_get_board_info()` for `GET /info?module=General&submodule=Board`
68
+ - `async_get_lan_info()` for `GET /info?module=General&submodule=Lan`
69
+ - `async_get_nodes()` for `GET /info/nodes`
70
+ - `async_get_diagnostics()` for `GET /info?module=Diag`
71
+ - `async_get_write_requests_remaining()` for `GET /info?module=General&submodule=PublicApi`
72
+ - `async_set_ventilation_state()` for `POST /action/nodes/{node}` with `SetVentilationState`
73
+
74
+ The model layer includes `ApiInfo`, `BoardInfo`, `LanInfo`, `Node`,
75
+ `NodeGeneralInfo`, `NodeVentilationInfo`, and `NodeSensorInfo`.
76
+
77
+ ## Development
78
+
79
+ Install the development dependencies and run the same checks as CI:
80
+
81
+ ```bash
82
+ pip install ".[dev]"
83
+ pytest
84
+ ruff check src tests
85
+ ruff format --check src tests
86
+ mypy src
87
+ bandit -r src -ll
88
+ pip-audit --desc on
89
+ ```
90
+
91
+ ## Validation
92
+
93
+ The current API surface was validated against a real Duco box during the first
94
+ development pass, covering:
95
+
96
+ - `GET /api`
97
+ - `GET /info?module=General&submodule=Board`
98
+ - `GET /info?module=General&submodule=Lan`
99
+ - `GET /info/nodes`
100
+ - `GET /info?module=General&submodule=PublicApi`
101
+ - `POST /action/nodes/{node}` with a no-op `SetVentilationState`
102
+
@@ -0,0 +1,10 @@
1
+ duco_connectivity/__init__.py,sha256=nV81Rmq4633lA5PDACkbTmhQnIiHoizppNkMSde4HWE,1005
2
+ duco_connectivity/client.py,sha256=OLLBaxfH6_S64AbjoR9vty9jB49iqZdVE0u8HwjbAyA,10009
3
+ duco_connectivity/exceptions.py,sha256=gTusGqE3diTu0upVi86mCTfgon27Tmn8XFbiO4teNpQ,605
4
+ duco_connectivity/models.py,sha256=5KistlqS-0Igc7y96gbM74y-MCjjhk36qE0entmhHlk,3553
5
+ duco_connectivity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ python_duco_connectivity-0.1.0.dist-info/licenses/LICENSE,sha256=K58DA270jGV1XeBcFR2URo4j_DeAgq__4RcOizKwXc0,1075
7
+ python_duco_connectivity-0.1.0.dist-info/METADATA,sha256=o5eqzL4VigNOfKMRJd-uK489PfFMqaZxu5XlDnw5PAE,3302
8
+ python_duco_connectivity-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ python_duco_connectivity-0.1.0.dist-info/top_level.txt,sha256=BR24lfzAC5yMbBvnDtD2d4QJu5rPfW6cLloZdrOoBvE,18
10
+ python_duco_connectivity-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronald van der Meer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ duco_connectivity