python-can-hub 0.2.2__py3-none-win_amd64.whl → 0.2.4__py3-none-win_amd64.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.
canhub/_native.py CHANGED
@@ -126,3 +126,14 @@ lib.canhub_send.argtypes = [c_void_p, POINTER(CanHubFrame)]
126
126
  def last_error(session):
127
127
  detail = lib.canhub_last_error(session)
128
128
  return detail.decode("utf-8", errors="replace") if detail else "unknown error"
129
+
130
+
131
+ def list_interfaces(session, timeout_ms, capacity=64):
132
+ while True:
133
+ buffer = (CanHubInterfaceInfo * capacity)()
134
+ count = lib.canhub_list(session, buffer, capacity, timeout_ms)
135
+ if count < 0:
136
+ raise OSError(last_error(session))
137
+ if count < capacity:
138
+ return [buffer[index] for index in range(count)]
139
+ capacity *= 2
canhub/bus.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """python-can backend over libcanhub: can.Bus(interface="canhub", ...)."""
2
2
 
3
3
  import ctypes
4
- from typing import Optional, Tuple
4
+ import os
5
+ from typing import List, Optional, Tuple
5
6
 
6
7
  from can import BusABC, CanInitializationError, CanOperationError, Message
7
8
 
@@ -49,22 +50,9 @@ class CanHubBus(BusABC):
49
50
  self._session = None
50
51
  self._writable = False
51
52
 
52
- if hub_fingerprint is not None:
53
- hub_fingerprint = str(hub_fingerprint)
54
- if not _is_fingerprint(hub_fingerprint):
55
- raise CanInitializationError(
56
- "hub_fingerprint must be 64 hex characters (it may have been "
57
- "mangled by python-can config value casting; pass it as a string)"
58
- )
59
-
60
- config = native.CanHubConnectConfig()
61
- config.struct_size = ctypes.sizeof(config)
62
- config.url = url.encode() if url else None
63
- config.state_directory = state_dir.encode() if state_dir else None
64
- config.certificate_path = identity_cert.encode() if identity_cert else None
65
- config.key_path = identity_key.encode() if identity_key else None
66
- config.hub_fingerprint = hub_fingerprint.encode() if hub_fingerprint else None
67
- config.connect_timeout_ms = DEFAULT_TIMEOUT_MS
53
+ config = self._build_connect_config(
54
+ url, identity_cert, identity_key, hub_fingerprint, state_dir, DEFAULT_TIMEOUT_MS
55
+ )
68
56
 
69
57
  self._session = native.lib.canhub_connect(ctypes.byref(config))
70
58
  if not self._session:
@@ -74,6 +62,62 @@ class CanHubBus(BusABC):
74
62
  self.channel_info = f"canhub {channel} via {url or 'unix socket'}"
75
63
  super().__init__(channel=channel, **kwargs)
76
64
 
65
+ @classmethod
66
+ def list_interfaces(
67
+ cls,
68
+ url: Optional[str] = None,
69
+ identity_cert: Optional[str] = None,
70
+ identity_key: Optional[str] = None,
71
+ hub_fingerprint: Optional[str] = None,
72
+ state_dir: Optional[str] = None,
73
+ timeout: float = DEFAULT_TIMEOUT_MS / 1000,
74
+ ) -> List[dict]:
75
+ """List the interfaces a hub exports, ready to splat into ``can.Bus``.
76
+
77
+ Connects to ``url`` (or the local hub unix socket when ``None``), asks the
78
+ hub for its interface directory and disconnects. Each entry is a python-can
79
+ config dict whose keys ``can.Bus(**entry)`` consumes directly. Connection
80
+ parameters mirror :class:`CanHubBus`.
81
+ """
82
+ timeout_ms = max(0, int(timeout * 1000))
83
+ config = cls._build_connect_config(
84
+ url, identity_cert, identity_key, hub_fingerprint, state_dir, timeout_ms
85
+ )
86
+
87
+ session = native.lib.canhub_connect(ctypes.byref(config))
88
+ if not session:
89
+ raise CanInitializationError(f"could not connect to {url or 'the local can-hub socket'}")
90
+
91
+ try:
92
+ interfaces = native.list_interfaces(session, timeout_ms)
93
+ except OSError as error:
94
+ raise CanOperationError(str(error)) from error
95
+ finally:
96
+ native.lib.canhub_close(session)
97
+
98
+ connection = {
99
+ "url": url,
100
+ "identity_cert": identity_cert,
101
+ "identity_key": identity_key,
102
+ "hub_fingerprint": hub_fingerprint,
103
+ "state_dir": state_dir,
104
+ }
105
+ present = {key: value for key, value in connection.items() if value is not None}
106
+ return [cls._to_config(info, present) for info in interfaces]
107
+
108
+ @classmethod
109
+ def _detect_available_configs(cls) -> List[dict]:
110
+ try:
111
+ return cls.list_interfaces(
112
+ url=os.environ.get("CANHUB_URL"),
113
+ identity_cert=os.environ.get("CANHUB_IDENTITY_CERT"),
114
+ identity_key=os.environ.get("CANHUB_IDENTITY_KEY"),
115
+ hub_fingerprint=os.environ.get("CANHUB_HUB_FINGERPRINT"),
116
+ state_dir=os.environ.get("CANHUB_STATE_DIR"),
117
+ )
118
+ except Exception:
119
+ return []
120
+
77
121
  def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], bool]:
78
122
  frame = native.CanHubFrame()
79
123
  timeout_ms = -1 if timeout is None else max(0, int(timeout * 1000))
@@ -151,6 +195,39 @@ class CanHubBus(BusABC):
151
195
  self._session = None
152
196
  raise CanInitializationError(f"could not open {channel}: {detail}")
153
197
 
198
+ @staticmethod
199
+ def _build_connect_config(
200
+ url: Optional[str],
201
+ identity_cert: Optional[str],
202
+ identity_key: Optional[str],
203
+ hub_fingerprint: Optional[str],
204
+ state_dir: Optional[str],
205
+ timeout_ms: int,
206
+ ) -> "native.CanHubConnectConfig":
207
+ if hub_fingerprint is not None:
208
+ hub_fingerprint = str(hub_fingerprint)
209
+ if not _is_fingerprint(hub_fingerprint):
210
+ raise CanInitializationError(
211
+ "hub_fingerprint must be 64 hex characters (it may have been "
212
+ "mangled by python-can config value casting; pass it as a string)"
213
+ )
214
+
215
+ config = native.CanHubConnectConfig()
216
+ config.struct_size = ctypes.sizeof(config)
217
+ config.url = url.encode() if url else None
218
+ config.state_directory = state_dir.encode() if state_dir else None
219
+ config.certificate_path = identity_cert.encode() if identity_cert else None
220
+ config.key_path = identity_key.encode() if identity_key else None
221
+ config.hub_fingerprint = hub_fingerprint.encode() if hub_fingerprint else None
222
+ config.connect_timeout_ms = timeout_ms
223
+ return config
224
+
225
+ @staticmethod
226
+ def _to_config(info: native.CanHubInterfaceInfo, connection: dict) -> dict:
227
+ agent = info.agent.decode("utf-8", errors="replace")
228
+ name = info.interface_name.decode("utf-8", errors="replace")
229
+ return {"interface": "canhub", "channel": f"{agent}/{name}", **connection}
230
+
154
231
  @staticmethod
155
232
  def _to_message(frame: native.CanHubFrame) -> Message:
156
233
  return Message(
canhub/libcanhub.dll CHANGED
Binary file
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-can-hub
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: python-can backend for can-hub: remote CAN interfaces over unix/tcp/tls/quic
5
5
  License: AGPL-3.0-only
6
6
  Project-URL: Homepage, https://github.com/can-hub-io/can-hub
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: python-can >=4.0
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest >=7 ; extra == 'test'
10
12
 
11
13
  # python-can-hub
12
14
 
@@ -38,9 +40,47 @@ for message in bus:
38
40
  Write access follows the hub client ACLs: if the ACL grants read-only, the
39
41
  bus opens read-only and `send()` raises.
40
42
 
43
+ ## Listing interfaces
44
+
45
+ `CanHubBus.list_interfaces()` asks a hub for the interfaces it exports. Each
46
+ entry is a python-can config dict you can splat straight into `can.Bus`:
47
+
48
+ ```python
49
+ from canhub import CanHubBus
50
+ import can
51
+
52
+ for config in CanHubBus.list_interfaces(url="quic://hub.example.com:7227"):
53
+ print(config["channel"]) # e.g. "can-agent/can0"
54
+ bus = can.Bus(**CanHubBus.list_interfaces()[0]) # first interface, local hub
55
+ ```
56
+
57
+ It accepts the same connection arguments as the bus (`url`, `identity_cert`,
58
+ `identity_key`, `hub_fingerprint`, `state_dir`); omit `url` to query the local
59
+ hub unix socket.
60
+
61
+ This also wires python-can's discovery: `can.detect_available_configs("canhub")`
62
+ returns the same dicts. With no explicit target it reads the connection from the
63
+ environment — `CANHUB_URL`, `CANHUB_STATE_DIR`, `CANHUB_IDENTITY_CERT`,
64
+ `CANHUB_IDENTITY_KEY`, `CANHUB_HUB_FINGERPRINT` — falling back to the local hub
65
+ unix socket. So `CANHUB_URL=quic://hub.example.com:7227` points discovery at a
66
+ remote host without any code.
67
+
41
68
  The wheel bundles `libcanhub.so` with the TLS/QUIC stack linked in
42
69
  statically; the only runtime dependency is glibc.
43
70
 
71
+ ## Testing
72
+
73
+ The unit tests stub the native library, so they run without a build:
74
+
75
+ ```sh
76
+ pip install -e python/.[test]
77
+ pytest python/tests
78
+ ```
79
+
80
+ The backend is also exercised end-to-end against real binaries (recv, send,
81
+ `list_interfaces`, and `can.detect_available_configs` discovery) by
82
+ `test/e2e/tests/python_can.robot`; run the whole bench with `make e2e`.
83
+
44
84
  ## Building from source
45
85
 
46
86
  ```sh
@@ -58,5 +98,6 @@ cross arches):
58
98
  ./scripts/build-python-wheel.sh armv7l
59
99
  ```
60
100
 
61
- The release workflow builds all three and publishes them to PyPI on a `v*`
62
- tag.
101
+ The release workflow builds these three on native runners (aarch64/armv7l on
102
+ the arm64 hosted runner, no QEMU) plus a cross-compiled win_amd64 wheel, and
103
+ publishes all four to PyPI on a `v*` tag.
@@ -0,0 +1,11 @@
1
+ canhub/__init__.py,sha256=yC7SFSz4vUDFgve7gehAaOSqZ1Iv1Kbv1A4c5nj_XcM,485
2
+ canhub/__main__.py,sha256=WM0VMTSjzySENh7zkbTT4WpLnqyS_gsZBgJ4f05qNHQ,366
3
+ canhub/_native.py,sha256=Dnk0z-fWQ3UkaoxXt4N6Ml7OHJQtXKSGtU-46CH56qo,3672
4
+ canhub/bus.py,sha256=6qv9tXgIA7hHIqnBJMewOyGyFxS-48Of9OaaJ9Abhrw,10085
5
+ canhub/fingerprint.py,sha256=N38dP3_odFYn6821XViWG6aQKUPhxJHDY8xE8uW8zNo,711
6
+ canhub/libcanhub.dll,sha256=xPTHIVUv-1ti0sMR6lA_8iePdFKK5dA9gzTKpd1CKgo,6398464
7
+ python_can_hub-0.2.4.dist-info/METADATA,sha256=t0735LhHCZw2lPs1EI_zNTg971bs8ymKQx05G8JZLZw,3588
8
+ python_can_hub-0.2.4.dist-info/WHEEL,sha256=KTdQDMVZqs2eeRhOpF4kInPW2OgLgRWl7KhVZQueA2o,99
9
+ python_can_hub-0.2.4.dist-info/entry_points.txt,sha256=ZfPo9cCR4xH9BjYRCi_nOcw_48SN6vqboV0B2y8HcjU,46
10
+ python_can_hub-0.2.4.dist-info/top_level.txt,sha256=nPyUT2Mw-NsKrJhpH-4EUcBBqz7GOSwfxnGkFEILnKk,7
11
+ python_can_hub-0.2.4.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- canhub/__init__.py,sha256=yC7SFSz4vUDFgve7gehAaOSqZ1Iv1Kbv1A4c5nj_XcM,485
2
- canhub/__main__.py,sha256=WM0VMTSjzySENh7zkbTT4WpLnqyS_gsZBgJ4f05qNHQ,366
3
- canhub/_native.py,sha256=4MHqnxR86NfMN_4jrp5rg60MR1hfhD7p-zXozSoAEAg,3295
4
- canhub/bus.py,sha256=dLwIGXdGplCyG_vh0Rb9DmM5kUjGCMyV_OlMU10q4x4,7060
5
- canhub/fingerprint.py,sha256=N38dP3_odFYn6821XViWG6aQKUPhxJHDY8xE8uW8zNo,711
6
- canhub/libcanhub.dll,sha256=BFcvsPyRdRgD2wiGAAlHZEDRXTqHQlHwjlTkDaxu19k,6396928
7
- python_can_hub-0.2.2.dist-info/METADATA,sha256=MaxFh9oRfrcQ60f7_UfbYdbz6Ng3RarPS0Om8fDaWVs,1998
8
- python_can_hub-0.2.2.dist-info/WHEEL,sha256=KTdQDMVZqs2eeRhOpF4kInPW2OgLgRWl7KhVZQueA2o,99
9
- python_can_hub-0.2.2.dist-info/entry_points.txt,sha256=ZfPo9cCR4xH9BjYRCi_nOcw_48SN6vqboV0B2y8HcjU,46
10
- python_can_hub-0.2.2.dist-info/top_level.txt,sha256=nPyUT2Mw-NsKrJhpH-4EUcBBqz7GOSwfxnGkFEILnKk,7
11
- python_can_hub-0.2.2.dist-info/RECORD,,