gbg-remote-sdk 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.
- gbg_remote_sdk-0.1.0/PKG-INFO +46 -0
- gbg_remote_sdk-0.1.0/README.md +32 -0
- gbg_remote_sdk-0.1.0/pyproject.toml +55 -0
- gbg_remote_sdk-0.1.0/setup.cfg +4 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/__init__.py +23 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/constants.py +28 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/exceptions.py +19 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/models.py +18 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/py.typed +0 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk/sdk.py +199 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk.egg-info/PKG-INFO +46 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk.egg-info/SOURCES.txt +13 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk.egg-info/dependency_links.txt +1 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk.egg-info/requires.txt +3 -0
- gbg_remote_sdk-0.1.0/src/gbg_remote_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gbg-remote-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Software Development Kit for the SOAP-based API for Green Button Go Remote Control
|
|
5
|
+
Author: Eli Fine
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.12.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: zeep>=4.3.2
|
|
12
|
+
Requires-Dist: pydantic>=2.11.7
|
|
13
|
+
Requires-Dist: requests>=2.32.5
|
|
14
|
+
|
|
15
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
16
|
+
[](https://github.com/astral-sh/ruff)
|
|
17
|
+
[](https://github.com/astral-sh/uv)
|
|
18
|
+
[](https://microsoft.github.io/pyright/)
|
|
19
|
+
[](https://github.com/lab-sync/gbg-remote-sdk/actions)
|
|
20
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk)
|
|
21
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
22
|
+
[](https://pepy.tech/project/gbg-remote-sdk)
|
|
23
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
24
|
+
[](https://codecov.io/gh/lab-sync/gbg-remote-sdk)
|
|
25
|
+
|
|
26
|
+
# Usage
|
|
27
|
+
Documentation is hosted on [ReadTheDocs](https://gbg-remote-sdk.readthedocs.io/en/latest/?badge=latest).
|
|
28
|
+
|
|
29
|
+
# Development
|
|
30
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
31
|
+
|
|
32
|
+
To publish a new version of the repository, you can run the `Publish` workflow manually and publish to the staging registry from any branch, and you can check the 'Publish to Primary' option when on `main` to publish to the primary registry and create a git tag.
|
|
33
|
+
|
|
34
|
+
### For doing testing of the GBG Remote interface:
|
|
35
|
+
Open a connection to the EC2 instance hosting GBG in AWS:
|
|
36
|
+
```bash
|
|
37
|
+
aws ssm start-session --target i-033eef3ccfbe82829 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8080"],"localPortNumber":["8080"]}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### When running the unit test suite, `pytest-recording` library is used. If it gives you errors about a cassette missing, the parameter to invoke a live HTTP request is `--record-mode=once` (add on to the end of the `pytest` command)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Updating from the template
|
|
45
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
46
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
2
|
+
[](https://github.com/astral-sh/ruff)
|
|
3
|
+
[](https://github.com/astral-sh/uv)
|
|
4
|
+
[](https://microsoft.github.io/pyright/)
|
|
5
|
+
[](https://github.com/lab-sync/gbg-remote-sdk/actions)
|
|
6
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk)
|
|
7
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
8
|
+
[](https://pepy.tech/project/gbg-remote-sdk)
|
|
9
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
10
|
+
[](https://codecov.io/gh/lab-sync/gbg-remote-sdk)
|
|
11
|
+
|
|
12
|
+
# Usage
|
|
13
|
+
Documentation is hosted on [ReadTheDocs](https://gbg-remote-sdk.readthedocs.io/en/latest/?badge=latest).
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
17
|
+
|
|
18
|
+
To publish a new version of the repository, you can run the `Publish` workflow manually and publish to the staging registry from any branch, and you can check the 'Publish to Primary' option when on `main` to publish to the primary registry and create a git tag.
|
|
19
|
+
|
|
20
|
+
### For doing testing of the GBG Remote interface:
|
|
21
|
+
Open a connection to the EC2 instance hosting GBG in AWS:
|
|
22
|
+
```bash
|
|
23
|
+
aws ssm start-session --target i-033eef3ccfbe82829 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8080"],"localPortNumber":["8080"]}'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### When running the unit test suite, `pytest-recording` library is used. If it gives you errors about a cassette missing, the parameter to invoke a live HTTP request is `--record-mode=once` (add on to the end of the `pytest` command)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Updating from the template
|
|
31
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
32
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gbg-remote-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Software Development Kit for the SOAP-based API for Green Button Go Remote Control"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Eli Fine"},
|
|
7
|
+
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12.7"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"zeep>=4.3.2",
|
|
21
|
+
"pydantic>=2.11.7",
|
|
22
|
+
"requests>=2.32.5",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
# Specific to this repository
|
|
29
|
+
"vcrpy>=7.0.0",
|
|
30
|
+
"pytest-recording>=0.13.4",
|
|
31
|
+
|
|
32
|
+
# Managed by upstream template
|
|
33
|
+
"pyright>=1.1.405",
|
|
34
|
+
"pytest>=8.4.2",
|
|
35
|
+
"pytest-cov>=7.0.0",
|
|
36
|
+
"pytest-randomly>=4.0.1",
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools]
|
|
42
|
+
license-files = [] # kludge until this bug is fixed https://github.com/pypa/setuptools/issues/4759
|
|
43
|
+
|
|
44
|
+
[tool.uv]
|
|
45
|
+
package = true
|
|
46
|
+
|
|
47
|
+
[[tool.uv.index]]
|
|
48
|
+
name = "pypi"
|
|
49
|
+
url = "https://pypi.org/simple/"
|
|
50
|
+
publish-url = "https://upload.pypi.org/legacy/"
|
|
51
|
+
|
|
52
|
+
[[tool.uv.index]]
|
|
53
|
+
name = "testpypi"
|
|
54
|
+
url = "https://test.pypi.org/simple/"
|
|
55
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .constants import INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX
|
|
2
|
+
from .constants import InstrumentName
|
|
3
|
+
from .constants import InstrumentStatus
|
|
4
|
+
from .constants import OrderStatus
|
|
5
|
+
from .constants import ProgramStatus
|
|
6
|
+
from .exceptions import GbgVariableNotFoundError
|
|
7
|
+
from .exceptions import NoGbgProgramLoadedError
|
|
8
|
+
from .exceptions import WorkcellProcessNotAvailableError
|
|
9
|
+
from .models import WorkcellProcessParameter
|
|
10
|
+
from .sdk import GbgRemoteSdk
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX",
|
|
14
|
+
"GbgRemoteSdk",
|
|
15
|
+
"GbgVariableNotFoundError",
|
|
16
|
+
"InstrumentName",
|
|
17
|
+
"InstrumentStatus",
|
|
18
|
+
"NoGbgProgramLoadedError",
|
|
19
|
+
"OrderStatus",
|
|
20
|
+
"ProgramStatus",
|
|
21
|
+
"WorkcellProcessNotAvailableError",
|
|
22
|
+
"WorkcellProcessParameter",
|
|
23
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX = ".Status"
|
|
4
|
+
type InstrumentName = str
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProgramStatus(str, Enum):
|
|
8
|
+
IDLE = "Idle"
|
|
9
|
+
RUNNING = "Running"
|
|
10
|
+
PAUSED = "Paused"
|
|
11
|
+
ERROR = "Error"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InstrumentStatus(str, Enum):
|
|
15
|
+
READY = "Ready"
|
|
16
|
+
BUSY = "Busy"
|
|
17
|
+
ERROR = "Error"
|
|
18
|
+
LOCKED = "Locked"
|
|
19
|
+
UNKNOWN = "Unknown"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrderStatus(str, Enum):
|
|
23
|
+
UNKNOWN = "Unkown" # yes, this is really misspelled in the API response # spellchecker:disable-line
|
|
24
|
+
CANCELED = "Canceled"
|
|
25
|
+
COMPLETE = "Complete"
|
|
26
|
+
RUNNING = "Running"
|
|
27
|
+
SCHEDULED = "Scheduled" # TODO: explicitly test this---but it was observed in the wild
|
|
28
|
+
ERROR = "Error" # guessing that this is possible in a fixed version of GBG, but not tested yet
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class GbgRemoteApiError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NoGbgProgramLoadedError(GbgRemoteApiError):
|
|
6
|
+
def __init__(self, *, attempted_action: str):
|
|
7
|
+
super().__init__(f"No GBG program is loaded. Attempted action cannot be completed: {attempted_action}")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkcellProcessNotAvailableError(GbgRemoteApiError):
|
|
11
|
+
def __init__(self, *, process_name: str, available_process_names: list[str]):
|
|
12
|
+
super().__init__(
|
|
13
|
+
f"Workcell process not available: {process_name}\nAvailable processes: {', '.join(available_process_names)}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GbgVariableNotFoundError(GbgRemoteApiError):
|
|
18
|
+
def __init__(self, variable_name: str):
|
|
19
|
+
super().__init__(f"Variable not found: {variable_name}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WorkcellProcessParameter(BaseModel):
|
|
5
|
+
variable_name: str
|
|
6
|
+
variable_value: bool | int | float | str | None
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def variable_value_in_gbg_format(self) -> str:
|
|
10
|
+
"""Convert the variable value to a string that GBG can understand."""
|
|
11
|
+
if self.variable_value is None:
|
|
12
|
+
return ""
|
|
13
|
+
if isinstance(self.variable_value, bool):
|
|
14
|
+
return "True" if self.variable_value else "False"
|
|
15
|
+
if isinstance(self.variable_value, (float, int)):
|
|
16
|
+
if int(self.variable_value) == self.variable_value:
|
|
17
|
+
return str(int(self.variable_value))
|
|
18
|
+
return str(self.variable_value)
|
|
File without changes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from ipaddress import IPv4Address
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from time import perf_counter_ns
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from zeep import Client
|
|
13
|
+
from zeep.exceptions import Fault
|
|
14
|
+
from zeep.transports import Transport
|
|
15
|
+
|
|
16
|
+
from .constants import INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX
|
|
17
|
+
from .constants import InstrumentName
|
|
18
|
+
from .constants import InstrumentStatus
|
|
19
|
+
from .constants import OrderStatus
|
|
20
|
+
from .constants import ProgramStatus
|
|
21
|
+
from .exceptions import GbgVariableNotFoundError
|
|
22
|
+
from .exceptions import NoGbgProgramLoadedError
|
|
23
|
+
from .exceptions import WorkcellProcessNotAvailableError
|
|
24
|
+
from .models import WorkcellProcessParameter
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GbgRemoteSdk:
|
|
30
|
+
def __init__(self, *, hostname: str | IPv4Address = "localhost", port: int = 8080, route: str = "/GBGRemote"):
|
|
31
|
+
super().__init__()
|
|
32
|
+
path_to_wsdl = Path(__file__).parent / "vendor_files" / "wsdl.xml"
|
|
33
|
+
# TODO: obtain the WSDL from the API endpoint and compare it to the local copy to ensure it matches expectations
|
|
34
|
+
with path_to_wsdl.open("r", encoding="utf-8") as f:
|
|
35
|
+
original_wsdl = f.read()
|
|
36
|
+
endpoint = f"http://{hostname!s}:{port}{route}"
|
|
37
|
+
wsdl_with_desired_endpoint = original_wsdl.replace("http://localhost:8080/GBGRemote", endpoint)
|
|
38
|
+
self._client = Client(
|
|
39
|
+
io.StringIO(wsdl_with_desired_endpoint),
|
|
40
|
+
transport=Transport(
|
|
41
|
+
timeout=0.5, # pyright: ignore[reportArgumentType] # zeep says it only wants ints, but it's just passing it to requests, which accepts floats
|
|
42
|
+
operation_timeout=3,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
self._short_timeout_client = Client(
|
|
46
|
+
self._client.wsdl,
|
|
47
|
+
transport=Transport(
|
|
48
|
+
timeout=0.5, # pyright: ignore[reportArgumentType] # zeep says it only wants ints, but it's just passing it to requests, which accepts floats
|
|
49
|
+
operation_timeout=0.5,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
self.hostname = str(hostname) # tried converting everything to Ipv4address, but it didn't work with localhost
|
|
53
|
+
self.port = port
|
|
54
|
+
self.route = route
|
|
55
|
+
|
|
56
|
+
# Known problems: GetStatus will return "Running" even when an error is thrown (later version of GBG fixes this)
|
|
57
|
+
# GetStatusDetails just appears to return an empty string (although maybe if there was an error, it would return something more meaningful)
|
|
58
|
+
# There's no way to specifically get a list of instruments in the workcell---the workaround is just check all variable names for things that end in `.status`
|
|
59
|
+
# There's no known way to tell if GBG is simulating an instrument within a workcell
|
|
60
|
+
def _invoke_request(self, request: Callable[[], Any]) -> Any: # noqa: ANN401 # yes, Any is generally bad, but zeep WSDL is terribly typed
|
|
61
|
+
start_time = perf_counter_ns()
|
|
62
|
+
response = request()
|
|
63
|
+
end_time = perf_counter_ns()
|
|
64
|
+
logger.debug( # TODO: figure out how to include the request name and parameters in the log
|
|
65
|
+
"Invoked GBG Remote API method ", extra={"gbg_remote_api_call_duration_ns": end_time - start_time}
|
|
66
|
+
)
|
|
67
|
+
return response
|
|
68
|
+
|
|
69
|
+
def is_simulated(self) -> bool:
|
|
70
|
+
try:
|
|
71
|
+
response = self._invoke_request(self._client.service.GetIsSimulated)
|
|
72
|
+
except Fault as e:
|
|
73
|
+
if "Object reference not set to an instance of an object" in str(e):
|
|
74
|
+
raise NoGbgProgramLoadedError(attempted_action="GetIsSimulated") from e
|
|
75
|
+
raise # pragma: no cover # not worth triggering a different type of error just to hit this default re-raise
|
|
76
|
+
response = cast(bool, response)
|
|
77
|
+
assert isinstance(response, bool), (
|
|
78
|
+
f"Expected response to be of type bool, but got type {type(response)} for {response}"
|
|
79
|
+
)
|
|
80
|
+
return response
|
|
81
|
+
|
|
82
|
+
def get_loaded_program(self) -> str | None:
|
|
83
|
+
response = cast(str, self._invoke_request(self._client.service.GetLoadedProgram))
|
|
84
|
+
assert isinstance(response, str), (
|
|
85
|
+
f"Expected response to be of type str, but got type {type(response)} for {response}"
|
|
86
|
+
)
|
|
87
|
+
if response == "None":
|
|
88
|
+
return None
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
def is_api_reachable(self) -> bool:
|
|
92
|
+
try:
|
|
93
|
+
self._invoke_request(
|
|
94
|
+
self._short_timeout_client.service.GetLoadedProgram
|
|
95
|
+
) # arbitrary API call that should always succeed
|
|
96
|
+
except requests.exceptions.ConnectionError:
|
|
97
|
+
return False
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def get_status(self) -> ProgramStatus:
|
|
101
|
+
response = cast(str, self._invoke_request(self._client.service.GetStatus))
|
|
102
|
+
assert isinstance(response, str), (
|
|
103
|
+
f"Expected response to be of type str, but got type {type(response)} for {response}"
|
|
104
|
+
)
|
|
105
|
+
return ProgramStatus(response)
|
|
106
|
+
|
|
107
|
+
def get_variable_names(self) -> list[str]:
|
|
108
|
+
response = self._invoke_request(self._client.service.GetVariableNames)
|
|
109
|
+
if (
|
|
110
|
+
response is None
|
|
111
|
+
): # there should never actually be no variables...this is what happens when no program is loaded
|
|
112
|
+
raise NoGbgProgramLoadedError(attempted_action="GetVariableNames")
|
|
113
|
+
response = cast(list[str], response)
|
|
114
|
+
assert isinstance(response, list), (
|
|
115
|
+
f"Expected response to be of type list, but got type {type(response)} for {response}"
|
|
116
|
+
)
|
|
117
|
+
return response
|
|
118
|
+
|
|
119
|
+
def get_variable_value(self, variable_name: str) -> str | None:
|
|
120
|
+
try:
|
|
121
|
+
response = self._invoke_request(partial(self._client.service.GetVariableValue, variable_name)) # pyright: ignore[reportUnknownArgumentType] # the WSDL is not typed
|
|
122
|
+
except Fault as e:
|
|
123
|
+
if "Get Variable Value could not find a variable named" in str(e):
|
|
124
|
+
raise GbgVariableNotFoundError(variable_name) from e
|
|
125
|
+
raise # pragma: no cover # not worth triggering a different type of error just to hit this default re-raise
|
|
126
|
+
if response is None:
|
|
127
|
+
return None
|
|
128
|
+
response = cast(str, response)
|
|
129
|
+
assert isinstance(response, str), (
|
|
130
|
+
f"Expected response to be of type str, but got type {type(response)} for {response}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return response
|
|
134
|
+
|
|
135
|
+
def get_instrument_statuses(self) -> dict[InstrumentName, InstrumentStatus]:
|
|
136
|
+
statuses: dict[InstrumentName, InstrumentStatus] = {}
|
|
137
|
+
try:
|
|
138
|
+
variable_names = self.get_variable_names()
|
|
139
|
+
except NoGbgProgramLoadedError as e:
|
|
140
|
+
raise NoGbgProgramLoadedError(attempted_action="GetInstrumentStatuses") from e
|
|
141
|
+
for variable_name in variable_names:
|
|
142
|
+
if variable_name.endswith(INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX):
|
|
143
|
+
instrument_name = variable_name[: -len(INSTRUMENT_STATUS_VARIABLE_NAME_SUFFIX)]
|
|
144
|
+
status = self.get_variable_value(variable_name)
|
|
145
|
+
statuses[instrument_name] = InstrumentStatus.UNKNOWN if status is None else InstrumentStatus(status)
|
|
146
|
+
return statuses
|
|
147
|
+
|
|
148
|
+
def start_workcell_process(
|
|
149
|
+
self, *, process_name: str, order_id: str, parameters: list[WorkcellProcessParameter]
|
|
150
|
+
) -> str:
|
|
151
|
+
"""Start a workcell process and return the RunID that was started."""
|
|
152
|
+
available_processes = self.get_available_workcell_processes()
|
|
153
|
+
if process_name not in available_processes:
|
|
154
|
+
raise WorkcellProcessNotAvailableError(
|
|
155
|
+
process_name=process_name, available_process_names=available_processes
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# many things were attempted to use native python data structures of list/dict, but nothing worked except this
|
|
159
|
+
array_type = self._client.get_type( # pyright: ignore[reportUnknownMemberType] # zeep WSDL is not typed
|
|
160
|
+
"{http://schemas.microsoft.com/2003/10/Serialization/Arrays}ArrayOfKeyValueOfstringstring"
|
|
161
|
+
)
|
|
162
|
+
assert array_type is not None, "Failed to get the ArrayOfKeyValueOfstringstring type from the WSDL"
|
|
163
|
+
|
|
164
|
+
parsed_parameters = array_type(
|
|
165
|
+
KeyValueOfstringstring=[
|
|
166
|
+
{"Key": param.variable_name, "Value": param.variable_value_in_gbg_format} for param in parameters
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
response = self._invoke_request(
|
|
171
|
+
partial( # pyright: ignore[reportUnknownArgumentType] # the WSDL is not typed
|
|
172
|
+
self._client.service.Run,
|
|
173
|
+
workcellProcessName=process_name,
|
|
174
|
+
orderId=order_id,
|
|
175
|
+
parameters=parsed_parameters,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
assert isinstance(response, str), (
|
|
179
|
+
f"Expected response to be of type str, but got type {type(response)} for {response}"
|
|
180
|
+
)
|
|
181
|
+
return response
|
|
182
|
+
|
|
183
|
+
def get_order_status(self, order_id: str) -> OrderStatus:
|
|
184
|
+
response = self._invoke_request(
|
|
185
|
+
partial( # pyright: ignore[reportUnknownArgumentType] # the WSDL is not typed
|
|
186
|
+
self._client.service.GetOrderStatus, orderId=order_id
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
assert isinstance(response, str), (
|
|
190
|
+
f"Expected response to be of type str, but got type {type(response)} for {response}"
|
|
191
|
+
)
|
|
192
|
+
return OrderStatus(response)
|
|
193
|
+
|
|
194
|
+
def get_available_workcell_processes(self) -> list[str]:
|
|
195
|
+
response = self._invoke_request(self._client.service.GetAvailableWorkcellProcesses)
|
|
196
|
+
assert isinstance(response, list), (
|
|
197
|
+
f"Expected response to be of type list, but got type {type(response)} for {response}"
|
|
198
|
+
)
|
|
199
|
+
return cast(list[str], response)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gbg-remote-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Software Development Kit for the SOAP-based API for Green Button Go Remote Control
|
|
5
|
+
Author: Eli Fine
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.12.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: zeep>=4.3.2
|
|
12
|
+
Requires-Dist: pydantic>=2.11.7
|
|
13
|
+
Requires-Dist: requests>=2.32.5
|
|
14
|
+
|
|
15
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
16
|
+
[](https://github.com/astral-sh/ruff)
|
|
17
|
+
[](https://github.com/astral-sh/uv)
|
|
18
|
+
[](https://microsoft.github.io/pyright/)
|
|
19
|
+
[](https://github.com/lab-sync/gbg-remote-sdk/actions)
|
|
20
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk)
|
|
21
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
22
|
+
[](https://pepy.tech/project/gbg-remote-sdk)
|
|
23
|
+
[](https://pypi.org/project/gbg-remote-sdk/)
|
|
24
|
+
[](https://codecov.io/gh/lab-sync/gbg-remote-sdk)
|
|
25
|
+
|
|
26
|
+
# Usage
|
|
27
|
+
Documentation is hosted on [ReadTheDocs](https://gbg-remote-sdk.readthedocs.io/en/latest/?badge=latest).
|
|
28
|
+
|
|
29
|
+
# Development
|
|
30
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/gbg-remote-sdk) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
31
|
+
|
|
32
|
+
To publish a new version of the repository, you can run the `Publish` workflow manually and publish to the staging registry from any branch, and you can check the 'Publish to Primary' option when on `main` to publish to the primary registry and create a git tag.
|
|
33
|
+
|
|
34
|
+
### For doing testing of the GBG Remote interface:
|
|
35
|
+
Open a connection to the EC2 instance hosting GBG in AWS:
|
|
36
|
+
```bash
|
|
37
|
+
aws ssm start-session --target i-033eef3ccfbe82829 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8080"],"localPortNumber":["8080"]}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### When running the unit test suite, `pytest-recording` library is used. If it gives you errors about a cassette missing, the parameter to invoke a live HTTP request is `--record-mode=once` (add on to the end of the `pytest` command)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Updating from the template
|
|
45
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
46
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gbg_remote_sdk/__init__.py
|
|
4
|
+
src/gbg_remote_sdk/constants.py
|
|
5
|
+
src/gbg_remote_sdk/exceptions.py
|
|
6
|
+
src/gbg_remote_sdk/models.py
|
|
7
|
+
src/gbg_remote_sdk/py.typed
|
|
8
|
+
src/gbg_remote_sdk/sdk.py
|
|
9
|
+
src/gbg_remote_sdk.egg-info/PKG-INFO
|
|
10
|
+
src/gbg_remote_sdk.egg-info/SOURCES.txt
|
|
11
|
+
src/gbg_remote_sdk.egg-info/dependency_links.txt
|
|
12
|
+
src/gbg_remote_sdk.egg-info/requires.txt
|
|
13
|
+
src/gbg_remote_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gbg_remote_sdk
|