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.
- pybricks_core-1.0.0b1/LICENSE +21 -0
- pybricks_core-1.0.0b1/PKG-INFO +42 -0
- pybricks_core-1.0.0b1/README.md +12 -0
- pybricks_core-1.0.0b1/pyproject.toml +52 -0
- pybricks_core-1.0.0b1/pyproject.toml.orig +40 -0
- pybricks_core-1.0.0b1/src/pb_core/__init__.py +13 -0
- pybricks_core-1.0.0b1/src/pb_core/auth.py +90 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/__init__.py +66 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/address.py +151 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/__init__.py +58 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/_anyio_listener.py +31 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/memory.py +163 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/posix.py +118 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/provider.py +104 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/tcp.py +111 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/backend/windows.py +237 -0
- pybricks_core-1.0.0b1/src/pb_core/ipc/errors.py +55 -0
- pybricks_core-1.0.0b1/src/pb_core/jsonrpc/__init__.py +60 -0
- pybricks_core-1.0.0b1/src/pb_core/jsonrpc/errors.py +104 -0
- pybricks_core-1.0.0b1/src/pb_core/jsonrpc/framing.py +120 -0
- pybricks_core-1.0.0b1/src/pb_core/jsonrpc/messages.py +271 -0
- pybricks_core-1.0.0b1/src/pb_core/jsonrpc/peer.py +435 -0
- pybricks_core-1.0.0b1/src/pb_core/lock.py +161 -0
- pybricks_core-1.0.0b1/src/pb_core/logging.py +242 -0
- pybricks_core-1.0.0b1/src/pb_core/modules.py +334 -0
- pybricks_core-1.0.0b1/src/pb_core/paths.py +185 -0
- pybricks_core-1.0.0b1/src/pb_core/project.py +344 -0
- pybricks_core-1.0.0b1/src/pb_core/protocol.py +134 -0
- 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()
|