gomc-rest 0.1.0__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.
- gomc_rest/__init__.py +103 -0
- gomc_rest/_binaries.py +70 -0
- gomc_rest/_server.py +137 -0
- gomc_rest/binaries/.gitignore +4 -0
- gomc_rest/binaries/README.md +15 -0
- gomc_rest/binaries/gomc-rest.exe +0 -0
- gomc_rest-0.1.0.dist-info/METADATA +156 -0
- gomc_rest-0.1.0.dist-info/RECORD +10 -0
- gomc_rest-0.1.0.dist-info/WHEEL +5 -0
- gomc_rest-0.1.0.dist-info/licenses/LICENSE +21 -0
gomc_rest/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""gomc-rest (Python, Pattern B).
|
|
2
|
+
|
|
3
|
+
Bundles the gomc-rest server binary and offers two entry points over the same
|
|
4
|
+
PLCClient (from gomc-rest-client):
|
|
5
|
+
|
|
6
|
+
* ``launch()`` — auto-start the bundled server as a subprocess (optionally
|
|
7
|
+
exposed to the network) and talk to it.
|
|
8
|
+
* ``connect()`` — act as a client to a gomc-rest server that is already
|
|
9
|
+
running elsewhere, without starting anything.
|
|
10
|
+
|
|
11
|
+
The HTTP layer is provided entirely by gomc-rest-client; this package only adds
|
|
12
|
+
binary bundling and process lifecycle.
|
|
13
|
+
|
|
14
|
+
import gomc_rest
|
|
15
|
+
|
|
16
|
+
with gomc_rest.launch(plc_host="192.168.0.1") as plc:
|
|
17
|
+
plc.read("D100", 3)
|
|
18
|
+
|
|
19
|
+
plc = gomc_rest.connect("http://192.168.0.1:8080", token="...")
|
|
20
|
+
plc.read("D100", 3)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from gomc_rest_client import (
|
|
26
|
+
GomcRestBusyError,
|
|
27
|
+
GomcRestPLCProtocolError,
|
|
28
|
+
GomcRestUnauthorizedError,
|
|
29
|
+
PLCClient,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ._server import Server
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def launch(
|
|
38
|
+
plc_host: str = "192.168.0.1",
|
|
39
|
+
plc_port: int = 5007,
|
|
40
|
+
server_mode: bool = False,
|
|
41
|
+
token: str | None = None,
|
|
42
|
+
extra_args: list[str] | None = None,
|
|
43
|
+
startup_timeout: float = 10.0,
|
|
44
|
+
) -> Server:
|
|
45
|
+
"""Start the bundled gomc-rest server and return a managed Server.
|
|
46
|
+
|
|
47
|
+
Use as a context manager to get the PLCClient and auto-stop the server::
|
|
48
|
+
|
|
49
|
+
with gomc_rest.launch(plc_host="192.168.0.1") as plc:
|
|
50
|
+
plc.read("D100", 3)
|
|
51
|
+
|
|
52
|
+
A random bearer token is generated per launch and required by the server,
|
|
53
|
+
so even another process on this host that learns the port cannot call the
|
|
54
|
+
API. Pass an explicit ``token`` to share with another app, or ``token=""``
|
|
55
|
+
to disable auth (closed-network use). The token is available as
|
|
56
|
+
``server.token``.
|
|
57
|
+
|
|
58
|
+
By default the server also binds to loopback only. Set ``server_mode=True``
|
|
59
|
+
to bind all interfaces and let other apps on the network call the REST API
|
|
60
|
+
(give them ``server.token``). The server has no TLS — only expose it on a
|
|
61
|
+
trusted network.
|
|
62
|
+
|
|
63
|
+
``extra_args`` are passed through to the gomc-rest binary (e.g.
|
|
64
|
+
``["-enable-remote"]`` or ``["-readonly"]``).
|
|
65
|
+
"""
|
|
66
|
+
return Server(
|
|
67
|
+
plc_host=plc_host,
|
|
68
|
+
plc_port=plc_port,
|
|
69
|
+
server_mode=server_mode,
|
|
70
|
+
token=token,
|
|
71
|
+
extra_args=extra_args,
|
|
72
|
+
startup_timeout=startup_timeout,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def connect(
|
|
77
|
+
base_url: str = "http://127.0.0.1:8080",
|
|
78
|
+
token: str | None = None,
|
|
79
|
+
) -> PLCClient:
|
|
80
|
+
"""Return a PLCClient for a gomc-rest server that is already running.
|
|
81
|
+
|
|
82
|
+
Use this to talk to a server started elsewhere — another machine, a shared
|
|
83
|
+
instance, or one you launched with ``server_mode=True`` — without spawning
|
|
84
|
+
the bundled binary. Supports the context-manager protocol::
|
|
85
|
+
|
|
86
|
+
with gomc_rest.connect("http://192.168.0.1:8080", token="...") as plc:
|
|
87
|
+
plc.read("D100", 3)
|
|
88
|
+
|
|
89
|
+
For the bundled, auto-started server use ``launch()`` instead.
|
|
90
|
+
"""
|
|
91
|
+
return PLCClient(base_url, token=token)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"launch",
|
|
96
|
+
"connect",
|
|
97
|
+
"Server",
|
|
98
|
+
"PLCClient",
|
|
99
|
+
"GomcRestBusyError",
|
|
100
|
+
"GomcRestPLCProtocolError",
|
|
101
|
+
"GomcRestUnauthorizedError",
|
|
102
|
+
"__version__",
|
|
103
|
+
]
|
gomc_rest/_binaries.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Locate the bundled gomc-rest server binary for the current platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import stat
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_BINARIES_DIR = Path(__file__).parent / "binaries"
|
|
10
|
+
|
|
11
|
+
# (system, machine) -> bundled binary file name.
|
|
12
|
+
# Machine names are normalised to the keys below before lookup.
|
|
13
|
+
_BINARY_NAMES = {
|
|
14
|
+
("windows", "amd64"): "gomc-rest.exe",
|
|
15
|
+
("linux", "amd64"): "gomc-rest-linux-amd64",
|
|
16
|
+
("linux", "arm64"): "gomc-rest-linux-arm64",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_MACHINE_ALIASES = {
|
|
20
|
+
"x86_64": "amd64",
|
|
21
|
+
"amd64": "amd64",
|
|
22
|
+
"aarch64": "arm64",
|
|
23
|
+
"arm64": "arm64",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalise_platform() -> tuple[str, str]:
|
|
28
|
+
system = platform.system().lower()
|
|
29
|
+
machine = _MACHINE_ALIASES.get(platform.machine().lower(), platform.machine().lower())
|
|
30
|
+
return system, machine
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def binary_path() -> Path:
|
|
34
|
+
"""Return the path to the bundled server binary for this platform.
|
|
35
|
+
|
|
36
|
+
Raises RuntimeError if the platform is unsupported or the binary is missing
|
|
37
|
+
from the package (e.g. a sdist install without the platform wheel).
|
|
38
|
+
"""
|
|
39
|
+
system, machine = _normalise_platform()
|
|
40
|
+
name = _BINARY_NAMES.get((system, machine))
|
|
41
|
+
if name is None:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
f"Unsupported platform {system}/{machine}. "
|
|
44
|
+
f"gomc-rest ships binaries for: {sorted(_BINARY_NAMES)}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
path = _BINARIES_DIR / name
|
|
48
|
+
if not path.exists():
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
f"Bundled server binary '{name}' was not found at {path}. "
|
|
51
|
+
"Install the platform-specific wheel, or place the binary there "
|
|
52
|
+
"(see gomc_rest/binaries/README.md)."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Wheels preserve the executable bit set at build time; only repair it when
|
|
56
|
+
# it is actually missing (e.g. an sdist/VCS checkout). Chmod'ing
|
|
57
|
+
# unconditionally breaks the common root-installs / non-root-runs setup,
|
|
58
|
+
# where the running user does not own an already-executable binary.
|
|
59
|
+
if system != "windows" and not path.stat().st_mode & stat.S_IXUSR:
|
|
60
|
+
try:
|
|
61
|
+
mode = path.stat().st_mode
|
|
62
|
+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
63
|
+
except PermissionError as exc:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f"Bundled binary {path} is not executable and its permissions "
|
|
66
|
+
"cannot be changed by this user. Reinstall the wheel or add the "
|
|
67
|
+
"executable bit."
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
return path
|
gomc_rest/_server.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Launch and manage the bundled gomc-rest server as a subprocess (Pattern B)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from gomc_rest_client import PLCClient
|
|
13
|
+
|
|
14
|
+
from ._binaries import binary_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _free_port() -> int:
|
|
18
|
+
"""Pick a currently-free TCP port on the loopback interface."""
|
|
19
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
20
|
+
sock.bind(("127.0.0.1", 0))
|
|
21
|
+
return sock.getsockname()[1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Server:
|
|
25
|
+
"""Owns a gomc-rest subprocess and a PLCClient pointed at it.
|
|
26
|
+
|
|
27
|
+
Prefer the context-manager form::
|
|
28
|
+
|
|
29
|
+
with Server(plc_host="192.168.0.1") as plc:
|
|
30
|
+
plc.read("D100", 3)
|
|
31
|
+
|
|
32
|
+
When not used as a context manager the subprocess is still terminated at
|
|
33
|
+
interpreter exit via atexit.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
plc_host: str = "192.168.0.1",
|
|
39
|
+
plc_port: int = 5007,
|
|
40
|
+
server_mode: bool = False,
|
|
41
|
+
token: str | None = None,
|
|
42
|
+
extra_args: list[str] | None = None,
|
|
43
|
+
startup_timeout: float = 10.0,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._port = _free_port()
|
|
46
|
+
self.base_url = f"http://127.0.0.1:{self._port}"
|
|
47
|
+
|
|
48
|
+
# A per-launch random token is generated by default, so even another
|
|
49
|
+
# process on this host that learns the port cannot call the API without
|
|
50
|
+
# it. token is None -> generate; token == "" -> disable auth; otherwise
|
|
51
|
+
# use the given token.
|
|
52
|
+
if token is None:
|
|
53
|
+
self.token = secrets.token_urlsafe(24)
|
|
54
|
+
elif token == "":
|
|
55
|
+
self.token = None
|
|
56
|
+
else:
|
|
57
|
+
self.token = token
|
|
58
|
+
|
|
59
|
+
# Default: bind to loopback only, so no other host can reach the
|
|
60
|
+
# server. server_mode binds all interfaces so other apps on the
|
|
61
|
+
# network can call the REST API (give them self.token).
|
|
62
|
+
listen = f":{self._port}" if server_mode else f"127.0.0.1:{self._port}"
|
|
63
|
+
args = [
|
|
64
|
+
str(binary_path()),
|
|
65
|
+
"-listen", listen,
|
|
66
|
+
"-host", plc_host,
|
|
67
|
+
"-port", str(plc_port),
|
|
68
|
+
*(extra_args or []),
|
|
69
|
+
]
|
|
70
|
+
# Pass the token via the environment, not argv, so it is not exposed in
|
|
71
|
+
# the process list / /proc/<pid>/cmdline. Set GOMCR_TOKEN explicitly
|
|
72
|
+
# (empty when disabled) so an inherited value can't enable auth on the
|
|
73
|
+
# server while the client stays unauthenticated.
|
|
74
|
+
env = os.environ.copy()
|
|
75
|
+
env["GOMCR_TOKEN"] = self.token or ""
|
|
76
|
+
self._proc = subprocess.Popen(args, env=env)
|
|
77
|
+
atexit.register(self.close)
|
|
78
|
+
self.client = PLCClient(self.base_url, token=self.token)
|
|
79
|
+
try:
|
|
80
|
+
self._wait_until_healthy(startup_timeout)
|
|
81
|
+
self._check_version()
|
|
82
|
+
except BaseException:
|
|
83
|
+
# Never leave the subprocess running if startup validation fails;
|
|
84
|
+
# the caller never receives a Server to close() itself.
|
|
85
|
+
self.close()
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def _wait_until_healthy(self, timeout: float) -> None:
|
|
89
|
+
deadline = time.monotonic() + timeout
|
|
90
|
+
while time.monotonic() < deadline:
|
|
91
|
+
if self._proc.poll() is not None:
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"gomc-rest exited during startup (code {self._proc.returncode})."
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
self.client.health()
|
|
97
|
+
return
|
|
98
|
+
except Exception:
|
|
99
|
+
time.sleep(0.1)
|
|
100
|
+
raise TimeoutError(f"gomc-rest did not become healthy within {timeout}s.")
|
|
101
|
+
|
|
102
|
+
def _check_version(self) -> None:
|
|
103
|
+
if not self.client.is_supported_version():
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"Bundled gomc-rest {self.client.version()} is not supported by "
|
|
106
|
+
"the installed gomc-rest-client. Update the bundled binary."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
"""Release the client session and terminate the subprocess.
|
|
111
|
+
|
|
112
|
+
Safe to call more than once.
|
|
113
|
+
"""
|
|
114
|
+
atexit.unregister(self.close)
|
|
115
|
+
|
|
116
|
+
client = getattr(self, "client", None)
|
|
117
|
+
if client is not None:
|
|
118
|
+
try:
|
|
119
|
+
client.__exit__(None, None, None)
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
proc = getattr(self, "_proc", None)
|
|
124
|
+
if proc is None or proc.poll() is not None:
|
|
125
|
+
return
|
|
126
|
+
proc.terminate()
|
|
127
|
+
try:
|
|
128
|
+
proc.wait(timeout=5)
|
|
129
|
+
except subprocess.TimeoutExpired:
|
|
130
|
+
proc.kill()
|
|
131
|
+
proc.wait()
|
|
132
|
+
|
|
133
|
+
def __enter__(self) -> PLCClient:
|
|
134
|
+
return self.client
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *exc) -> None:
|
|
137
|
+
self.close()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Bundled server binaries
|
|
2
|
+
|
|
3
|
+
This directory holds the platform-specific `gomc-rest` server binaries that the
|
|
4
|
+
Python package auto-launches (Pattern B):
|
|
5
|
+
|
|
6
|
+
| Platform | File name |
|
|
7
|
+
| --------------- | ------------------------ |
|
|
8
|
+
| Windows (amd64) | `gomc-rest.exe` |
|
|
9
|
+
| Linux (amd64) | `gomc-rest-linux-amd64` |
|
|
10
|
+
| Linux (arm64) | `gomc-rest-linux-arm64` |
|
|
11
|
+
|
|
12
|
+
The binaries are **not committed to git**. They are vendored in at release time
|
|
13
|
+
from a pinned [gomc-rest](https://github.com/Moge800/gomc-rest) version, and each
|
|
14
|
+
platform wheel ships only its matching binary. `_binaries.py` resolves the
|
|
15
|
+
correct file for the current platform at runtime.
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gomc-rest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Talk to Mitsubishi PLCs from Python over the MC protocol via a bundled, auto-launched gomc-rest server — with optional REST API exposure.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Moge800/gomc_rest_python
|
|
6
|
+
Project-URL: Upstream server, https://github.com/Moge800/gomc-rest
|
|
7
|
+
Project-URL: HTTP client, https://github.com/Moge800/gomc_rest_client
|
|
8
|
+
Author: Moge800
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: gomc-rest,mc-protocol,mitsubishi,plc,slmp
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: gomc-rest-client<0.11,>=0.10.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# gomc-rest (Python)
|
|
17
|
+
|
|
18
|
+
English / [日本語](https://github.com/Moge800/gomc_rest_python/blob/main/README_JP.md)
|
|
19
|
+
|
|
20
|
+
**A Python library for talking to Mitsubishi PLCs.** Read and write PLC devices
|
|
21
|
+
over the MC protocol (3E/4E frames) — the protocol is handled for you by a bundled
|
|
22
|
+
[gomc-rest](https://github.com/Moge800/gomc-rest) server that the package
|
|
23
|
+
auto-launches, so you never have to start or distribute an executable yourself.
|
|
24
|
+
|
|
25
|
+
**Bonus — expose a REST API.** Enable `server_mode` to make the bundled
|
|
26
|
+
server's REST API reachable from other apps on your network (a GUI, another
|
|
27
|
+
machine, another language); and `connect()` talks to a gomc-rest server that is
|
|
28
|
+
already running elsewhere. See [Access control](#access-control).
|
|
29
|
+
|
|
30
|
+
Under the hood the HTTP layer is provided by
|
|
31
|
+
[gomc-rest-client](https://github.com/Moge800/gomc_rest_client); this package
|
|
32
|
+
adds the bundled binary and its process lifecycle.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
your Python process
|
|
36
|
+
└─ gomc_rest.launch()
|
|
37
|
+
├─ spawns the bundled gomc-rest on a free loopback port ── MC protocol ──▶ PLC
|
|
38
|
+
└─ returns a Server that provides a PLCClient pointed at it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install gomc-rest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import gomc_rest
|
|
51
|
+
|
|
52
|
+
with gomc_rest.launch(plc_host="192.168.0.1") as plc:
|
|
53
|
+
values = plc.read("D100", 3)
|
|
54
|
+
plc.write("D100", [10, 20, 30])
|
|
55
|
+
# the bundled server is stopped automatically on exit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`launch()` returns a `Server`; using it as a context manager yields a
|
|
59
|
+
`PLCClient` (see gomc-rest-client for the full read/write/remote API) and stops
|
|
60
|
+
the server on exit. Without `with`, the server is stopped at interpreter exit.
|
|
61
|
+
|
|
62
|
+
Pass server flags through `extra_args`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
with gomc_rest.launch(plc_host="192.168.0.1", extra_args=["-enable-remote"]) as plc:
|
|
66
|
+
plc.remote_run()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Client mode (connect to an existing server)
|
|
70
|
+
|
|
71
|
+
To talk to a gomc-rest server that is already running elsewhere — a shared
|
|
72
|
+
instance, another machine, or one you launched with `server_mode=True` — use
|
|
73
|
+
`connect()` instead of starting the bundled binary:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
with gomc_rest.connect("http://192.168.0.1:8080", token="...") as plc:
|
|
77
|
+
plc.read("D100", 3)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Both `launch()` and `connect()` hand you the same `PLCClient`, so one package
|
|
81
|
+
covers "bundle and run the server" and "just be a client".
|
|
82
|
+
|
|
83
|
+
`connect()` needs no bundled binary, so it works even on platforms without a
|
|
84
|
+
prebuilt wheel (macOS, Windows arm64, glibc < 2.34): there `pip install
|
|
85
|
+
gomc-rest` installs from the sdist, and only `launch()` is unavailable (it
|
|
86
|
+
raises a clear error).
|
|
87
|
+
|
|
88
|
+
## Access control
|
|
89
|
+
|
|
90
|
+
Two independent layers protect the server, both on by default:
|
|
91
|
+
|
|
92
|
+
1. **Per-launch bearer token.** A random token is generated each launch and
|
|
93
|
+
required by the server, so even another process on the same host that
|
|
94
|
+
discovers the port cannot call the API. It is set automatically on the
|
|
95
|
+
returned client and exposed as `server.token`. Pass an explicit `token=` to
|
|
96
|
+
share with another app, or `token=""` to disable auth (closed-network use).
|
|
97
|
+
2. **Loopback binding.** By default the server binds to `127.0.0.1`, so no
|
|
98
|
+
other host can reach it.
|
|
99
|
+
|
|
100
|
+
Set `server_mode=True` to bind all interfaces so other apps on the network
|
|
101
|
+
(e.g. gomc-rest-gui, curl from another machine) can call it — give them
|
|
102
|
+
`server.token`:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
server = gomc_rest.launch(plc_host="192.168.0.1", server_mode=True)
|
|
106
|
+
print(server.base_url) # other apps connect to http://<this-host>:<port>
|
|
107
|
+
print(server.token) # ...with this bearer token
|
|
108
|
+
try:
|
|
109
|
+
server.client.read("D100", 3)
|
|
110
|
+
finally:
|
|
111
|
+
server.close()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The server has no TLS — only enable `server_mode` on a trusted network.
|
|
115
|
+
|
|
116
|
+
**Threat model.** The token is passed to the server via the `GOMCR_TOKEN`
|
|
117
|
+
environment variable (not the command line), so it does not appear in the
|
|
118
|
+
process list. It protects against other hosts and other OS users. It does
|
|
119
|
+
**not** protect against another process running as the **same OS user**, which
|
|
120
|
+
can read the server's environment (e.g. `/proc/<pid>/environ`); the OS user
|
|
121
|
+
boundary is the trust boundary here.
|
|
122
|
+
|
|
123
|
+
## Versions
|
|
124
|
+
|
|
125
|
+
This package bundles a pinned `gomc-rest` binary (currently **v1.4.0**, set in
|
|
126
|
+
`GOMC_REST_VERSION`) that must satisfy `gomc-rest-client`'s
|
|
127
|
+
`MINIMUM_SUPPORTED_GOMC_REST_VERSION`; `launch()` verifies this on startup. The
|
|
128
|
+
`gomc-rest-client` dependency is capped (`>=0.10.0,<0.11`) so a future client
|
|
129
|
+
that raises its minimum server version can't be installed without also bumping
|
|
130
|
+
the bundled binary.
|
|
131
|
+
|
|
132
|
+
## Releasing / bundled binaries
|
|
133
|
+
|
|
134
|
+
The bundled server version is pinned in `GOMC_REST_VERSION`. Binaries are not
|
|
135
|
+
committed to git; they are fetched from the matching gomc-rest GitHub release.
|
|
136
|
+
|
|
137
|
+
- Locally: `python scripts/vendor_binaries.py` downloads all three binaries
|
|
138
|
+
into `src/gomc_rest/binaries/`, verifying each against the trusted SHA-256
|
|
139
|
+
values committed in `checksums/<version>.sha256`.
|
|
140
|
+
- On a `v*` tag, `.github/workflows/release.yml` builds one platform-specific
|
|
141
|
+
wheel per OS (each bundling only its matching binary) and publishes to PyPI
|
|
142
|
+
via trusted publishing. The release job verifies the tag equals
|
|
143
|
+
`project.version`; `workflow_dispatch` builds wheels for verification only and
|
|
144
|
+
never publishes.
|
|
145
|
+
|
|
146
|
+
To cut a release:
|
|
147
|
+
|
|
148
|
+
1. To change the bundled server, edit `GOMC_REST_VERSION` (keep it within the
|
|
149
|
+
range accepted by the pinned `gomc-rest-client`) and add a matching
|
|
150
|
+
`checksums/<version>.sha256` with the trusted SHA-256 of each asset. If the
|
|
151
|
+
new binaries change their glibc requirement, update the `plat` tags in
|
|
152
|
+
`release.yml` accordingly.
|
|
153
|
+
2. Bump the package version in **both** `pyproject.toml` (`project.version`)
|
|
154
|
+
and `src/gomc_rest/__init__.py` (`__version__`) — they must match.
|
|
155
|
+
3. Tag that exact version, e.g. `git tag v0.2.0 && git push origin v0.2.0`
|
|
156
|
+
(the tag must equal `project.version` or the release job fails).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gomc_rest/__init__.py,sha256=-sTEjHSEuOnept3GO28dl3-PoqLxSECbKa5lsferUl4,3252
|
|
2
|
+
gomc_rest/_binaries.py,sha256=wYhp6VcO5VfmztM2oIZWIVF_QHL0ADixkpCpuPwQ4qk,2521
|
|
3
|
+
gomc_rest/_server.py,sha256=GztYu0DVn3lqSJhjH9-8EkOfpMzJbeumVgtBA3SWx9M,4748
|
|
4
|
+
gomc_rest/binaries/.gitignore,sha256=iLgtA0Ah3vJKRQIDAoelvPL2Msv251d-qTqdFAt6dyo,111
|
|
5
|
+
gomc_rest/binaries/README.md,sha256=pfAsf-Bwa5_NOitEvLKXfp2ROKgQRjmlc35bnyM_1N4,688
|
|
6
|
+
gomc_rest/binaries/gomc-rest.exe,sha256=TTIzwuSsgwQDWOLvJD2GcGy99hQfkvi2Jw-PMrdKEeU,11051520
|
|
7
|
+
gomc_rest-0.1.0.dist-info/METADATA,sha256=R1w5UHJfQ8ury7TqA39QwxLm8_elUc9XvYYBh2TN-c0,6439
|
|
8
|
+
gomc_rest-0.1.0.dist-info/WHEEL,sha256=pp_iTzO0EBcCvlZZV2dJYp6L2tdDWBsn1ceo50wEt8Q,94
|
|
9
|
+
gomc_rest-0.1.0.dist-info/licenses/LICENSE,sha256=z2FbQ_Nsco0Ai59dmX5-WcROjekvhYSkiud0M7Q00Kg,1085
|
|
10
|
+
gomc_rest-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moge800
|
|
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.
|