gomc-rest 0.1.0__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.
@@ -0,0 +1,78 @@
1
+ # CLAUDE.md
2
+
3
+ Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
4
+
5
+ **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
6
+
7
+ ## 1. Think Before Coding
8
+
9
+ **Don't assume. Don't hide confusion. Surface tradeoffs.**
10
+
11
+ Before implementing:
12
+ - State your assumptions explicitly. If uncertain, ask.
13
+ - If multiple interpretations exist, present them - don't pick silently.
14
+ - If a simpler approach exists, say so. Push back when warranted.
15
+ - If something is unclear, stop. Name what's confusing. Ask.
16
+
17
+ ## 2. Simplicity First
18
+
19
+ **Minimum code that solves the problem. Nothing speculative.**
20
+
21
+ - No features beyond what was asked.
22
+ - No abstractions for single-use code.
23
+ - No "flexibility" or "configurability" that wasn't requested.
24
+ - No error handling for impossible scenarios.
25
+ - If you write 200 lines and it could be 50, rewrite it.
26
+
27
+ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
28
+
29
+ ## 3. Surgical Changes
30
+
31
+ **Touch only what you must. Clean up only your own mess.**
32
+
33
+ When editing existing code:
34
+ - Don't "improve" adjacent code, comments, or formatting.
35
+ - Don't refactor things that aren't broken.
36
+ - Match existing style, even if you'd do it differently.
37
+ - If you notice unrelated dead code, mention it - don't delete it.
38
+
39
+ When your changes create orphans:
40
+ - Remove imports/variables/functions that YOUR changes made unused.
41
+ - Don't remove pre-existing dead code unless asked.
42
+
43
+ The test: Every changed line should trace directly to the user's request.
44
+
45
+ ## 4. Goal-Driven Execution
46
+
47
+ **Define success criteria. Loop until verified.**
48
+
49
+ Transform tasks into verifiable goals:
50
+ - "Add validation" → "Write tests for invalid inputs, then make them pass"
51
+ - "Fix the bug" → "Write a test that reproduces it, then make it pass"
52
+ - "Refactor X" → "Ensure tests pass before and after"
53
+
54
+ For multi-step tasks, state a brief plan:
55
+ ```
56
+ 1. [Step] → verify: [check]
57
+ 2. [Step] → verify: [check]
58
+ 3. [Step] → verify: [check]
59
+ ```
60
+
61
+ Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
62
+
63
+ ---
64
+
65
+ **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
66
+
67
+
68
+ ---
69
+ **Stream Timeout Prevention**
70
+ - Do each task ONE AT A TIME. Complete one, confirm, then move to the next.
71
+ - Never write a file longer than ~150 lines in a single tool call.
72
+ - If a file will be longer, write it in multiple append/edit passes.
73
+ - Start a fresh session if the conversation gets long (20+ tool calls).
74
+ - The error gets worse as the session grows.
75
+ - Keep individual grep/search outputs short. Use flags like
76
+ - --include and -l (list files only) to limit output size.
77
+ - If you do hit the timeout, retry the same step in a shorter form.
78
+ - Don't repeat the entire task from scratch.
@@ -0,0 +1,41 @@
1
+ name: ci
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.11"
17
+
18
+ - name: Install package and test deps
19
+ run: python -m pip install -e . pytest
20
+
21
+ - name: Vendor the Linux server binary
22
+ run: python scripts/vendor_binaries.py --only gomc-rest-linux-amd64
23
+
24
+ - name: Run tests
25
+ run: python -m pytest -v
26
+
27
+ # Verify the dynamically-linked amd64 binary actually starts on its declared
28
+ # glibc floor (manylinux_2_34 == glibc 2.34), not just on the latest runner.
29
+ floor-glibc-amd64:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - name: Smoke test on glibc 2.34
34
+ run: |
35
+ docker run --rm -v "$PWD":/w -w /w \
36
+ quay.io/pypa/manylinux_2_34_x86_64 bash -c '
37
+ PY=/opt/python/cp311-cp311/bin/python
38
+ "$PY" -m pip install -e . pytest &&
39
+ "$PY" scripts/vendor_binaries.py --only gomc-rest-linux-amd64 &&
40
+ "$PY" -m pytest -v
41
+ '
@@ -0,0 +1,114 @@
1
+ name: release
2
+
3
+ # Build one platform-specific wheel per OS (each bundling only its matching
4
+ # gomc-rest binary) plus a binary-free sdist, and publish to PyPI on a version
5
+ # tag. The sdist is the client-only fallback: on platforms without a matching
6
+ # wheel (macOS, Windows arm64, glibc < 2.34), pip builds it so connect() still
7
+ # works (launch() then raises a clear "binary not found/unsupported" error).
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ build-wheels:
16
+ name: wheel (${{ matrix.plat }})
17
+ runs-on: ${{ matrix.os }}
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ include:
22
+ - os: windows-latest
23
+ asset: gomc-rest.exe
24
+ plat: win_amd64
25
+ # amd64 binary is dynamically linked and needs GLIBC_2.34, so it must
26
+ # be tagged manylinux_2_34 (not 2014/glibc 2.17) or pip would install
27
+ # it on older systems where it fails with "GLIBC_2.34 not found".
28
+ - os: ubuntu-latest
29
+ asset: gomc-rest-linux-amd64
30
+ plat: manylinux_2_34_x86_64
31
+ # arm64 binary is statically linked (no glibc dependency), so the
32
+ # widest valid tag is fine.
33
+ - os: ubuntu-24.04-arm
34
+ asset: gomc-rest-linux-arm64
35
+ plat: manylinux2014_aarch64
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+
39
+ - uses: actions/setup-python@v5
40
+ with:
41
+ python-version: "3.11"
42
+
43
+ - name: Check tag matches project version
44
+ if: github.ref_type == 'tag'
45
+ shell: bash
46
+ run: |
47
+ tag="${GITHUB_REF_NAME#v}"
48
+ proj=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_bytes().decode())['project']['version'])")
49
+ echo "tag=$tag project.version=$proj"
50
+ test "$tag" = "$proj" || { echo "::error::tag $GITHUB_REF_NAME does not match project.version $proj"; exit 1; }
51
+
52
+ - name: Install build tooling
53
+ run: python -m pip install --upgrade build wheel
54
+
55
+ - name: Vendor the matching server binary
56
+ run: python scripts/vendor_binaries.py --only ${{ matrix.asset }}
57
+
58
+ - name: Build wheel
59
+ run: python -m build --wheel
60
+
61
+ - name: Re-tag wheel for this platform
62
+ shell: bash
63
+ run: python -m wheel tags --platform-tag ${{ matrix.plat }} --remove dist/*.whl
64
+
65
+ - uses: actions/upload-artifact@v4
66
+ with:
67
+ name: wheel-${{ matrix.plat }}
68
+ path: dist/*.whl
69
+
70
+ build-sdist:
71
+ name: sdist (client-only fallback)
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+
76
+ - uses: actions/setup-python@v5
77
+ with:
78
+ python-version: "3.11"
79
+
80
+ - name: Check tag matches project version
81
+ if: github.ref_type == 'tag'
82
+ shell: bash
83
+ run: |
84
+ tag="${GITHUB_REF_NAME#v}"
85
+ proj=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_bytes().decode())['project']['version'])")
86
+ test "$tag" = "$proj" || { echo "::error::tag $GITHUB_REF_NAME does not match project.version $proj"; exit 1; }
87
+
88
+ - name: Build sdist (no binary bundled)
89
+ run: |
90
+ python -m pip install --upgrade build
91
+ python -m build --sdist
92
+
93
+ - uses: actions/upload-artifact@v4
94
+ with:
95
+ name: sdist
96
+ path: dist/*.tar.gz
97
+
98
+ publish:
99
+ name: publish to PyPI
100
+ needs: [build-wheels, build-sdist]
101
+ # Only publish for real version tags; workflow_dispatch builds wheels for
102
+ # verification but must never publish from an arbitrary branch.
103
+ if: github.ref_type == 'tag'
104
+ runs-on: ubuntu-latest
105
+ environment: pypi
106
+ permissions:
107
+ id-token: write # trusted publishing (OIDC) — no API token needed
108
+ steps:
109
+ - uses: actions/download-artifact@v4
110
+ with:
111
+ path: dist
112
+ merge-multiple: true
113
+
114
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
@@ -0,0 +1 @@
1
+ v1.4.0
@@ -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.
@@ -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).