pybricks-core 1.0.0b1__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 (29) hide show
  1. pybricks_core-1.0.0b1/LICENSE +21 -0
  2. pybricks_core-1.0.0b1/PKG-INFO +42 -0
  3. pybricks_core-1.0.0b1/README.md +12 -0
  4. pybricks_core-1.0.0b1/pyproject.toml +52 -0
  5. pybricks_core-1.0.0b1/pyproject.toml.orig +40 -0
  6. pybricks_core-1.0.0b1/src/pb_core/__init__.py +13 -0
  7. pybricks_core-1.0.0b1/src/pb_core/auth.py +90 -0
  8. pybricks_core-1.0.0b1/src/pb_core/ipc/__init__.py +66 -0
  9. pybricks_core-1.0.0b1/src/pb_core/ipc/address.py +151 -0
  10. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/__init__.py +58 -0
  11. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/_anyio_listener.py +31 -0
  12. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/memory.py +163 -0
  13. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/posix.py +118 -0
  14. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/provider.py +104 -0
  15. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/tcp.py +111 -0
  16. pybricks_core-1.0.0b1/src/pb_core/ipc/backend/windows.py +237 -0
  17. pybricks_core-1.0.0b1/src/pb_core/ipc/errors.py +55 -0
  18. pybricks_core-1.0.0b1/src/pb_core/jsonrpc/__init__.py +60 -0
  19. pybricks_core-1.0.0b1/src/pb_core/jsonrpc/errors.py +104 -0
  20. pybricks_core-1.0.0b1/src/pb_core/jsonrpc/framing.py +120 -0
  21. pybricks_core-1.0.0b1/src/pb_core/jsonrpc/messages.py +271 -0
  22. pybricks_core-1.0.0b1/src/pb_core/jsonrpc/peer.py +435 -0
  23. pybricks_core-1.0.0b1/src/pb_core/lock.py +161 -0
  24. pybricks_core-1.0.0b1/src/pb_core/logging.py +242 -0
  25. pybricks_core-1.0.0b1/src/pb_core/modules.py +334 -0
  26. pybricks_core-1.0.0b1/src/pb_core/paths.py +185 -0
  27. pybricks_core-1.0.0b1/src/pb_core/project.py +344 -0
  28. pybricks_core-1.0.0b1/src/pb_core/protocol.py +134 -0
  29. pybricks_core-1.0.0b1/src/pb_core/py.typed +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Pybricks Authors
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,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybricks-core
3
+ Version: 1.0.0b1
4
+ Summary: Shared protocol, IPC transport and logging for the Pybricks tools
5
+ Keywords: pybricks,lego,micropython,bluetooth,robotics
6
+ Author: The Pybricks Authors
7
+ Author-email: The Pybricks Authors <dev@pybricks.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: anyio>=4.14
18
+ Requires-Dist: platformdirs>=4
19
+ Requires-Dist: typing-extensions>=4.15
20
+ Maintainer: David Lechner
21
+ Maintainer-email: David Lechner <david@pybricks.com>
22
+ Requires-Python: >=3.11
23
+ Project-URL: homepage, https://pybricks.com
24
+ Project-URL: repository, https://github.com/pybricks/pybricks-tools
25
+ Project-URL: documentation, https://pybricks.com/pybricks-tools/design/
26
+ Project-URL: Changelog, https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-core/CHANGELOG.md
27
+ Project-URL: Support, https://github.com/pybricks/support/discussions
28
+ Project-URL: Issues, https://github.com/pybricks/support/issues
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pybricks-core
32
+
33
+ The pieces shared by the [Pybricks](https://pybricks.com) tools: the JSON-RPC
34
+ protocol the client and the service speak, the transports underneath it, the
35
+ compiler that turns a script into something a hub can run, and the logging and
36
+ file locations all of them use.
37
+
38
+ **You do not install this yourself.** It comes with `pybricks-cli` and with
39
+ `pybricks-client`, which is the one to reach for if you want to drive a hub from
40
+ your own Python.
41
+
42
+ > **Status:** beta. Commands and APIs may still change before 1.0.0.
@@ -0,0 +1,12 @@
1
+ # pybricks-core
2
+
3
+ The pieces shared by the [Pybricks](https://pybricks.com) tools: the JSON-RPC
4
+ protocol the client and the service speak, the transports underneath it, the
5
+ compiler that turns a script into something a hub can run, and the logging and
6
+ file locations all of them use.
7
+
8
+ **You do not install this yourself.** It comes with `pybricks-cli` and with
9
+ `pybricks-client`, which is the one to reach for if you want to drive a hub from
10
+ your own Python.
11
+
12
+ > **Status:** beta. Commands and APIs may still change before 1.0.0.
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "pybricks-core"
3
+ version = "1.0.0b1"
4
+ description = "Shared protocol, IPC transport and logging for the Pybricks tools"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "pybricks",
11
+ "lego",
12
+ "micropython",
13
+ "bluetooth",
14
+ "robotics",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Education",
20
+ "Natural Language :: English",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Software Development :: Embedded Systems",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "anyio>=4.14",
27
+ "platformdirs>=4",
28
+ "typing-extensions>=4.15",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "The Pybricks Authors"
33
+ email = "dev@pybricks.com"
34
+
35
+ [[project.maintainers]]
36
+ name = "David Lechner"
37
+ email = "david@pybricks.com"
38
+
39
+ [project.urls]
40
+ homepage = "https://pybricks.com"
41
+ repository = "https://github.com/pybricks/pybricks-tools"
42
+ documentation = "https://pybricks.com/pybricks-tools/design/"
43
+ Changelog = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-core/CHANGELOG.md"
44
+ Support = "https://github.com/pybricks/support/discussions"
45
+ Issues = "https://github.com/pybricks/support/issues"
46
+
47
+ [build-system]
48
+ requires = ["uv_build>=0.12.7,<0.13.0"]
49
+ build-backend = "uv_build"
50
+
51
+ [tool.uv.build-backend]
52
+ module-name = "pb_core"
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "pybricks-core"
3
+ version = "1.0.0b1"
4
+ description = "Shared protocol, IPC transport and logging for the Pybricks tools"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "The Pybricks Authors", email = "dev@pybricks.com" }]
9
+ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }]
10
+ requires-python = ">=3.11"
11
+ keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: Education",
16
+ "Natural Language :: English",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Embedded Systems",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "anyio>=4.14",
23
+ "platformdirs>=4",
24
+ "typing-extensions>=4.15",
25
+ ]
26
+
27
+ [project.urls]
28
+ homepage = "https://pybricks.com"
29
+ repository = "https://github.com/pybricks/pybricks-tools"
30
+ documentation = "https://pybricks.com/pybricks-tools/design/"
31
+ "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-core/CHANGELOG.md"
32
+ "Support" = "https://github.com/pybricks/support/discussions"
33
+ "Issues" = "https://github.com/pybricks/support/issues"
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.12.7,<0.13.0"]
37
+ build-backend = "uv_build"
38
+
39
+ [tool.uv.build-backend]
40
+ module-name = "pb_core"
@@ -0,0 +1,13 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Shared building blocks for the Pybricks tool suite.
5
+
6
+ This package holds everything both the service and the client need to agree on:
7
+ the JSON-RPC wire protocol, the per-platform IPC transport, logging setup and
8
+ platform paths. It deliberately depends on neither ``pb_service`` nor ``pb_client``.
9
+ """
10
+
11
+ __version__ = "1.0.0b1"
12
+
13
+ __all__ = ["__version__"]
@@ -0,0 +1,90 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Shared-secret authentication between the client and the service.
5
+
6
+ The service writes a random token to a file only the current user can read, and
7
+ every client presents it in the mandatory first call. This is the X11
8
+ magic-cookie scheme.
9
+
10
+ It is required on every transport rather than only on TCP. The default
11
+ discretionary access control list on a Windows named pipe grants Everyone read
12
+ access, so without a secret another local user could not issue commands but
13
+ could still watch a hub's output. It is not sufficient on its own for a service
14
+ reachable beyond loopback, where the token crosses the network in clear and
15
+ needs TLS or an SSH tunnel around it.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import secrets
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from pb_core import paths
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ __all__ = ["TOKEN_BYTES", "load_token", "read_or_create_token"]
30
+
31
+ TOKEN_BYTES = 32
32
+
33
+
34
+ def read_or_create_token(path: Path | None = None) -> str:
35
+ """Return the service's token, creating one if there is none.
36
+
37
+ Called by the service at startup.
38
+
39
+ Args:
40
+ path: Override the cookie file location.
41
+
42
+ Returns:
43
+ The token as hexadecimal text.
44
+ """
45
+ cookie = path or paths.cookie_file()
46
+ paths.ensure_private_dir(cookie.parent)
47
+
48
+ if cookie.exists():
49
+ existing = cookie.read_text(encoding="ascii").strip()
50
+ if existing:
51
+ return existing
52
+
53
+ token = secrets.token_hex(TOKEN_BYTES)
54
+ # Create with restrictive permissions from the outset rather than widening
55
+ # then narrowing, which would leave a window where the token is readable.
56
+ descriptor = cookie.open("w", encoding="ascii")
57
+
58
+ try:
59
+ if sys.platform != "win32":
60
+ import os
61
+
62
+ os.fchmod(descriptor.fileno(), 0o600)
63
+
64
+ descriptor.write(token)
65
+ finally:
66
+ descriptor.close()
67
+
68
+ logger.debug("wrote a new authentication token to %s", cookie)
69
+ return token
70
+
71
+
72
+ def load_token(path: Path | None = None) -> str | None:
73
+ """Return the service's token if it can be read.
74
+
75
+ Called by clients before connecting.
76
+
77
+ Args:
78
+ path: Override the cookie file location.
79
+
80
+ Returns:
81
+ The token, or ``None`` if no service has ever run for this user.
82
+ """
83
+ cookie = path or paths.cookie_file()
84
+
85
+ try:
86
+ token = cookie.read_text(encoding="ascii").strip()
87
+ except OSError:
88
+ return None
89
+
90
+ return token or None
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Transport between the client and the service.
5
+
6
+ The user never types an address: :func:`pb_core.ipc.address.resolve` picks the
7
+ right one for the platform. Everything above this package deals only in
8
+ :class:`anyio.abc.ByteStream`, so neither the service nor the client contains any
9
+ platform-specific code.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextlib import AbstractAsyncContextManager
15
+
16
+ from anyio.abc import ByteStream
17
+
18
+ from pb_core.ipc.address import ENDPOINT_ENV_VAR, Endpoint, default, resolve
19
+ from pb_core.ipc.backend import IpcBackend, IpcListener, StreamHandler, get_backend
20
+ from pb_core.ipc.errors import AddressInUseError, EndpointError
21
+
22
+ __all__ = [
23
+ "ENDPOINT_ENV_VAR",
24
+ "AddressInUseError",
25
+ "Endpoint",
26
+ "EndpointError",
27
+ "IpcBackend",
28
+ "IpcListener",
29
+ "StreamHandler",
30
+ "connect",
31
+ "create_listener",
32
+ "default",
33
+ "get_backend",
34
+ "resolve",
35
+ ]
36
+
37
+
38
+ def connect(endpoint: Endpoint) -> AbstractAsyncContextManager[ByteStream]:
39
+ """Connect to the service listening at ``endpoint``.
40
+
41
+ Args:
42
+ endpoint: Where the service is listening.
43
+
44
+ Returns:
45
+ An async context manager yielding the connected stream.
46
+
47
+ Raises:
48
+ ConnectionRefusedError: If nothing is listening. Callers treat this as
49
+ "the service is not running" and may start one.
50
+ """
51
+ return get_backend(endpoint).connect(endpoint)
52
+
53
+
54
+ def create_listener(endpoint: Endpoint) -> AbstractAsyncContextManager[IpcListener]:
55
+ """Listen for clients at ``endpoint``.
56
+
57
+ Args:
58
+ endpoint: Where to listen.
59
+
60
+ Returns:
61
+ An async context manager yielding the listener.
62
+
63
+ Raises:
64
+ AddressInUseError: If another service already holds the address.
65
+ """
66
+ return get_backend(endpoint).create_listener(endpoint)
@@ -0,0 +1,151 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Endpoint addressing for the service.
5
+
6
+ An endpoint is written as a URI so that one string can name any transport:
7
+
8
+ * ``unix:///run/user/1000/pybricks/pb-service.sock`` -- AF_UNIX socket (Linux, macOS)
9
+ * ``pipe://pybricks-pb-service-david`` -- Windows named pipe
10
+ * ``tcp://127.0.0.1:47823`` -- TCP, for remote services and for tests
11
+ * ``memory://name`` -- in-process, used by the test suite
12
+
13
+ Users are not expected to type these. :func:`resolve` picks the right default
14
+ for the platform, and ``PB_ENDPOINT`` overrides it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sys
21
+ from dataclasses import dataclass
22
+ from typing import Literal
23
+
24
+ from pb_core import paths
25
+ from pb_core.ipc.errors import EndpointError
26
+
27
+ __all__ = ["ENDPOINT_ENV_VAR", "Endpoint", "Scheme", "default", "resolve"]
28
+
29
+ ENDPOINT_ENV_VAR = "PB_ENDPOINT"
30
+
31
+ Scheme = Literal["unix", "pipe", "tcp", "memory"]
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Endpoint:
36
+ """A transport-independent address for the service."""
37
+
38
+ scheme: Scheme
39
+ target: str
40
+ """Socket path, pipe name, ``host:port``, or in-process key."""
41
+
42
+ @classmethod
43
+ def parse(cls, uri: str) -> Endpoint:
44
+ """Parse an endpoint URI.
45
+
46
+ Args:
47
+ uri: A string such as ``tcp://127.0.0.1:47823``.
48
+
49
+ Returns:
50
+ The parsed endpoint.
51
+
52
+ Raises:
53
+ EndpointError: If the URI is malformed or names an unknown
54
+ scheme. It is a :class:`ValueError`.
55
+ """
56
+ scheme, separator, rest = uri.partition("://")
57
+ if not separator:
58
+ raise EndpointError(f"endpoint is not a URI: {uri!r}")
59
+
60
+ if scheme not in ("unix", "pipe", "tcp", "memory"):
61
+ raise EndpointError(f"unknown endpoint scheme: {scheme!r}")
62
+
63
+ if not rest:
64
+ raise EndpointError(f"endpoint has no target: {uri!r}")
65
+
66
+ if scheme == "tcp":
67
+ host, colon, port = rest.rpartition(":")
68
+ if not colon or not host:
69
+ raise EndpointError(f"tcp endpoint needs host:port: {uri!r}")
70
+
71
+ try:
72
+ number = int(port)
73
+ except ValueError:
74
+ raise EndpointError(
75
+ f"tcp endpoint port is not a number: {uri!r}"
76
+ ) from None
77
+
78
+ if not 0 <= number <= 65535:
79
+ raise EndpointError(f"tcp endpoint port out of range: {uri!r}")
80
+
81
+ return cls(scheme, rest)
82
+
83
+ @property
84
+ def tcp_address(self) -> tuple[str, int]:
85
+ """Return ``(host, port)`` for a TCP endpoint.
86
+
87
+ Raises:
88
+ ValueError: If this is not a TCP endpoint.
89
+ """
90
+
91
+ if self.scheme != "tcp":
92
+ raise EndpointError(f"not a tcp endpoint: {self}")
93
+
94
+ host, _, port = self.target.rpartition(":")
95
+ return host, int(port)
96
+
97
+ @property
98
+ def is_loopback(self) -> bool:
99
+ """Whether a TCP endpoint is confined to this machine.
100
+
101
+ Non-loopback TCP carries the shared secret over a plaintext socket, so
102
+ callers should require TLS or an SSH tunnel for it.
103
+ """
104
+
105
+ if self.scheme != "tcp":
106
+ return True
107
+
108
+ host, _ = self.tcp_address
109
+ return host in ("127.0.0.1", "::1", "localhost")
110
+
111
+ def __str__(self) -> str:
112
+ """Render the endpoint back as a URI."""
113
+ return f"{self.scheme}://{self.target}"
114
+
115
+
116
+ def default() -> Endpoint:
117
+ """Return the platform's default endpoint.
118
+
119
+ Windows uses a named pipe because asyncio cannot serve AF_UNIX sockets on
120
+ the proactor event loop, which the service requires for other reasons.
121
+ """
122
+
123
+ if sys.platform == "win32":
124
+ return Endpoint(
125
+ "pipe", paths.windows_pipe_name().removeprefix(r"\.\pipe" + "\\")
126
+ )
127
+
128
+ return Endpoint("unix", str(paths.socket_path()))
129
+
130
+
131
+ def resolve(uri: str | None = None) -> Endpoint:
132
+ """Return the endpoint to use.
133
+
134
+ Precedence is the explicit argument, then ``PB_ENDPOINT``, then the
135
+ platform default.
136
+
137
+ Args:
138
+ uri: An explicit endpoint URI, if the caller has one.
139
+
140
+ Returns:
141
+ The endpoint to connect to or listen on.
142
+ """
143
+
144
+ if uri:
145
+ return Endpoint.parse(uri)
146
+
147
+ from_env = os.environ.get(ENDPOINT_ENV_VAR)
148
+ if from_env:
149
+ return Endpoint.parse(from_env)
150
+
151
+ return default()
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Per-transport backends, selected by endpoint scheme."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from pb_core.ipc.address import Endpoint
11
+ from pb_core.ipc.backend.provider import IpcBackend, IpcListener, StreamHandler
12
+
13
+ __all__ = ["IpcBackend", "IpcListener", "StreamHandler", "get_backend"]
14
+
15
+
16
+ def get_backend(endpoint: Endpoint) -> IpcBackend:
17
+ """Return the backend that implements ``endpoint``.
18
+
19
+ Backends are imported lazily so that, for example, importing the client on
20
+ Linux never touches the Windows pipe module.
21
+
22
+ Args:
23
+ endpoint: The endpoint to be connected to or served.
24
+
25
+ Returns:
26
+ A backend for that endpoint's scheme.
27
+
28
+ Raises:
29
+ NotImplementedError: If the scheme is not supported on this platform.
30
+ """
31
+
32
+ if endpoint.scheme == "memory":
33
+ from pb_core.ipc.backend.memory import MemoryBackend
34
+
35
+ return MemoryBackend()
36
+
37
+ if endpoint.scheme == "tcp":
38
+ from pb_core.ipc.backend.tcp import TcpBackend
39
+
40
+ return TcpBackend()
41
+
42
+ if endpoint.scheme == "pipe":
43
+ if sys.platform != "win32":
44
+ raise NotImplementedError("named pipes are only available on Windows")
45
+
46
+ from pb_core.ipc.backend.windows import WindowsPipeBackend
47
+
48
+ return WindowsPipeBackend()
49
+
50
+ if sys.platform == "win32":
51
+ raise NotImplementedError(
52
+ "asyncio cannot serve AF_UNIX sockets on the Windows proactor loop; "
53
+ "use a pipe:// endpoint"
54
+ )
55
+
56
+ from pb_core.ipc.backend.posix import PosixBackend
57
+
58
+ return PosixBackend()
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Adapter presenting an anyio listener through the :class:`IpcListener` API."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, final
9
+
10
+ from anyio.abc import Listener, TaskGroup
11
+ from typing_extensions import override
12
+
13
+ from pb_core.ipc.backend.provider import IpcListener, StreamHandler, owned
14
+
15
+ __all__ = ["AnyioListener"]
16
+
17
+
18
+ @final
19
+ class AnyioListener(IpcListener):
20
+ """Wraps :class:`anyio.abc.Listener` so all backends share one interface."""
21
+
22
+ def __init__(self, inner: Listener[Any]) -> None:
23
+ self._inner = inner
24
+
25
+ @override
26
+ async def serve(self, handler: StreamHandler, task_group: TaskGroup) -> None:
27
+ await self._inner.serve(owned(handler), task_group)
28
+
29
+ @override
30
+ async def aclose(self) -> None:
31
+ await self._inner.aclose()