@uns-kit/cli 0.0.32 → 0.0.35
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.
- package/package.json +2 -2
- package/templates/api/src/examples/api-example.ts +1 -0
- package/templates/cron/src/examples/cron-example.ts +1 -0
- package/templates/default/config.json +3 -1
- package/templates/default/src/examples/table-example.ts +2 -1
- package/templates/python/app/README.md +8 -0
- package/templates/python/app/__init__.py +0 -0
- package/templates/python/examples/README.md +90 -40
- package/templates/python/examples/__init__.py +0 -0
- package/templates/python/examples/api_handler.py +26 -99
- package/templates/python/examples/data_publish.py +11 -0
- package/templates/python/examples/data_subscribe.py +6 -122
- package/templates/python/examples/data_transformer.py +13 -143
- package/templates/python/examples/table_transformer.py +14 -161
- package/templates/python/gateway/__init__.py +0 -0
- package/templates/python/gateway/cli.py +75 -0
- package/templates/python/gateway/client.py +155 -0
- package/templates/python/gateway/manager.py +97 -0
- package/templates/python/main.py +1 -0
- package/templates/python/pyproject.toml +5 -0
- package/templates/python/scripts/setup.sh +37 -14
- package/templates/temporal/src/examples/temporal-example.ts +1 -0
- package/templates/python/examples/api_register_and_serve.py +0 -159
- package/templates/python/examples/data_publish_once.py +0 -140
- package/templates/python/examples/data_publisher_loop.py +0 -142
- package/templates/python/gateway_client.py +0 -242
- package/templates/python/local/README.md +0 -8
- package/templates/python/local/__init__.py +0 -2
- package/templates/python/requirements.txt +0 -3
|
@@ -1,163 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
1
|
+
from gateway.client import Client, iso_now
|
|
7
2
|
import time
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from typing import Iterable, Dict
|
|
10
|
-
import socket
|
|
11
|
-
|
|
12
|
-
import grpc
|
|
13
|
-
|
|
14
|
-
import sys, os
|
|
15
|
-
# Ensure generated stubs (python/gen) are importable as top-level modules
|
|
16
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gen')))
|
|
17
|
-
import uns_gateway_pb2 as pb2
|
|
18
|
-
import uns_gateway_pb2_grpc as gw
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def iso_from_epoch_ms(ms: int) -> str:
|
|
22
|
-
dt = datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc)
|
|
23
|
-
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def make_channel(addr: str) -> grpc.Channel:
|
|
27
|
-
return grpc.insecure_channel(addr)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import atexit, signal, subprocess
|
|
31
|
-
_AUTO_GATEWAY_PROC = None
|
|
32
|
-
def _cleanup_gateway():
|
|
33
|
-
global _AUTO_GATEWAY_PROC
|
|
34
|
-
if _AUTO_GATEWAY_PROC and _AUTO_GATEWAY_PROC.poll() is None:
|
|
35
|
-
try:
|
|
36
|
-
if os.name == 'nt':
|
|
37
|
-
_AUTO_GATEWAY_PROC.terminate()
|
|
38
|
-
else:
|
|
39
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGTERM)
|
|
40
|
-
try:
|
|
41
|
-
_AUTO_GATEWAY_PROC.wait(timeout=3)
|
|
42
|
-
except Exception:
|
|
43
|
-
if os.name == 'nt':
|
|
44
|
-
_AUTO_GATEWAY_PROC.kill()
|
|
45
|
-
else:
|
|
46
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGKILL)
|
|
47
|
-
except Exception:
|
|
48
|
-
pass
|
|
49
|
-
_AUTO_GATEWAY_PROC = None
|
|
50
|
-
|
|
51
|
-
def _default_addr() -> str:
|
|
52
|
-
if os.name == 'nt':
|
|
53
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
54
|
-
s.bind(('127.0.0.1', 0))
|
|
55
|
-
port = s.getsockname()[1]
|
|
56
|
-
return f"127.0.0.1:{port}"
|
|
57
|
-
else:
|
|
58
|
-
return f"unix:/tmp/uns-gateway-table-transformer-{os.getpid()}.sock"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def ensure_gateway_running(addr: str | None, *, auto: bool, timeout_s: int = 20) -> str:
|
|
62
|
-
if not addr:
|
|
63
|
-
addr = _default_addr()
|
|
64
|
-
ch = make_channel(addr)
|
|
65
|
-
try:
|
|
66
|
-
grpc.channel_ready_future(ch).result(timeout=2)
|
|
67
|
-
ch.close(); return addr
|
|
68
|
-
except Exception:
|
|
69
|
-
ch.close()
|
|
70
|
-
if not auto:
|
|
71
|
-
return addr
|
|
72
|
-
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
73
|
-
cli_path = os.path.join(repo_root, "dist", "uns-grpc", "uns-gateway-cli")
|
|
74
|
-
popen_kwargs = {}
|
|
75
|
-
creationflags = 0
|
|
76
|
-
if os.name != 'nt':
|
|
77
|
-
popen_kwargs['preexec_fn'] = os.setsid
|
|
78
|
-
else:
|
|
79
|
-
creationflags = getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
|
80
|
-
suffix = f"py-{os.path.basename(__file__).replace('.py','')}-{os.getpid()}"
|
|
81
|
-
proc = subprocess.Popen(["node", cli_path, "--addr", addr, "--instanceSuffix", suffix, "--instanceMode", "force"], cwd=repo_root, creationflags=creationflags, **popen_kwargs)
|
|
82
|
-
global _AUTO_GATEWAY_PROC
|
|
83
|
-
_AUTO_GATEWAY_PROC = proc
|
|
84
|
-
atexit.register(_cleanup_gateway)
|
|
85
|
-
start = time.time()
|
|
86
|
-
while time.time() - start < timeout_s:
|
|
87
|
-
ch2 = make_channel(addr)
|
|
88
|
-
try:
|
|
89
|
-
grpc.channel_ready_future(ch2).result(timeout=2)
|
|
90
|
-
ch2.close();
|
|
91
|
-
try:
|
|
92
|
-
wait_s = int(os.environ.get("UNS_GATEWAY_HANDOVER_WAIT", "11"))
|
|
93
|
-
except Exception:
|
|
94
|
-
wait_s = 11
|
|
95
|
-
if wait_s > 0:
|
|
96
|
-
time.sleep(wait_s)
|
|
97
|
-
return addr
|
|
98
|
-
except Exception:
|
|
99
|
-
ch2.close(); time.sleep(0.5)
|
|
100
|
-
raise RuntimeError("Gateway did not become ready in time")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def transform(addr: str | None, in_topic: str, out_topic: str, out_attribute: str, auto: bool) -> None:
|
|
104
|
-
addr = ensure_gateway_running(addr, auto=auto)
|
|
105
|
-
# Ensure both subscriber and publisher are ready
|
|
106
|
-
try:
|
|
107
|
-
with make_channel(addr) as ch:
|
|
108
|
-
gw.UnsGatewayStub(ch).Ready(pb2.ReadyRequest(timeout_ms=15000, wait_input=True, wait_output=True))
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
111
|
-
with make_channel(addr) as ch:
|
|
112
|
-
stub = gw.UnsGatewayStub(ch)
|
|
113
|
-
stream = stub.Subscribe(pb2.SubscribeRequest(topics=[in_topic]))
|
|
114
|
-
for msg in stream:
|
|
115
|
-
if msg.topic != in_topic:
|
|
116
|
-
continue
|
|
117
|
-
try:
|
|
118
|
-
obj = json.loads(msg.payload)
|
|
119
|
-
except Exception:
|
|
120
|
-
continue
|
|
121
|
-
ts = obj.pop("Timestamp", None)
|
|
122
|
-
if ts is None:
|
|
123
|
-
continue
|
|
124
|
-
try:
|
|
125
|
-
ts_ms = int(ts)
|
|
126
|
-
except Exception:
|
|
127
|
-
continue
|
|
128
|
-
time_iso = iso_from_epoch_ms(ts_ms if ts_ms > 10_000_000_000 else ts_ms * 1000)
|
|
129
|
-
|
|
130
|
-
values: Dict[str, pb2.TableValue] = {}
|
|
131
|
-
tv_list = []
|
|
132
|
-
for k, v in obj.items():
|
|
133
|
-
if isinstance(v, (int, float)):
|
|
134
|
-
tv_list.append(pb2.TableValue(key=k, value_number=float(v)))
|
|
135
|
-
elif v is None:
|
|
136
|
-
tv_list.append(pb2.TableValue(key=k))
|
|
137
|
-
else:
|
|
138
|
-
tv_list.append(pb2.TableValue(key=k, value_string=str(v)))
|
|
139
|
-
|
|
140
|
-
req = pb2.PublishRequest(
|
|
141
|
-
topic=out_topic,
|
|
142
|
-
attribute=out_attribute,
|
|
143
|
-
table=pb2.Table(time=time_iso, values=tv_list, data_group="demo_table"),
|
|
144
|
-
)
|
|
145
|
-
res = stub.Publish(req)
|
|
146
|
-
if not res.ok:
|
|
147
|
-
print("publish error:", res.error)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def main():
|
|
151
|
-
parser = argparse.ArgumentParser(description="Subscribe to an upstream topic and publish UNS table packets")
|
|
152
|
-
parser.add_argument("--addr", default=None, help="Gateway address (unix:/path.sock or host:port); defaults to unique per-script")
|
|
153
|
-
parser.add_argument("--auto", action="store_true", help="auto-start gateway if not running")
|
|
154
|
-
parser.add_argument("--in-topic", default="integration/raw-table")
|
|
155
|
-
parser.add_argument("--out-topic", default="example/factory-a/line-1/")
|
|
156
|
-
parser.add_argument("--attribute", default="table-sample")
|
|
157
|
-
args = parser.parse_args()
|
|
158
|
-
|
|
159
|
-
transform(args.addr, args.in_topic, args.out_topic, args.attribute, args.auto)
|
|
160
|
-
|
|
161
3
|
|
|
162
|
-
|
|
163
|
-
|
|
4
|
+
client = Client()
|
|
5
|
+
client.publish_table(
|
|
6
|
+
topic="sensors/summary/",
|
|
7
|
+
attribute="room1",
|
|
8
|
+
obj={
|
|
9
|
+
"temperature": 22.5,
|
|
10
|
+
"humidity": 55.2,
|
|
11
|
+
"status": "ok",
|
|
12
|
+
"error": None
|
|
13
|
+
},
|
|
14
|
+
data_group="room_stats"
|
|
15
|
+
)
|
|
16
|
+
time.sleep(10)
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from .manager import GatewayManager
|
|
3
|
+
from . import client
|
|
4
|
+
import uns_gateway_pb2_grpc as gw
|
|
5
|
+
from .client import make_channel
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser()
|
|
9
|
+
parser.add_argument("--addr", default=None)
|
|
10
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
11
|
+
|
|
12
|
+
# --- Publish ---
|
|
13
|
+
p_pub = sub.add_parser("pub")
|
|
14
|
+
p_pub.add_argument("topic")
|
|
15
|
+
p_pub.add_argument("attribute")
|
|
16
|
+
p_pub.add_argument("time_iso")
|
|
17
|
+
p_pub.add_argument("value", type=float)
|
|
18
|
+
p_pub.add_argument("--uom", default="")
|
|
19
|
+
p_pub.add_argument("--group", default="")
|
|
20
|
+
p_pub.add_argument("--cumulative", action="store_true")
|
|
21
|
+
p_pub.add_argument("--auto", action="store_true")
|
|
22
|
+
|
|
23
|
+
# --- Subscribe ---
|
|
24
|
+
p_sub = sub.add_parser("sub")
|
|
25
|
+
p_sub.add_argument("topics", nargs="+")
|
|
26
|
+
p_sub.add_argument("--auto", action="store_true")
|
|
27
|
+
|
|
28
|
+
# --- Register API ---
|
|
29
|
+
p_reg = sub.add_parser("regapi")
|
|
30
|
+
p_reg.add_argument("topic")
|
|
31
|
+
p_reg.add_argument("attribute")
|
|
32
|
+
p_reg.add_argument("--desc", default="")
|
|
33
|
+
p_reg.add_argument("--tag", action="append", default=[])
|
|
34
|
+
p_reg.add_argument("--param", action="append", default=[])
|
|
35
|
+
p_reg.add_argument("--auto", action="store_true")
|
|
36
|
+
|
|
37
|
+
# --- Unregister API ---
|
|
38
|
+
p_unreg = sub.add_parser("unregapi")
|
|
39
|
+
p_unreg.add_argument("topic")
|
|
40
|
+
p_unreg.add_argument("attribute")
|
|
41
|
+
p_unreg.add_argument("--auto", action="store_true")
|
|
42
|
+
|
|
43
|
+
# --- API Stream ---
|
|
44
|
+
p_stream = sub.add_parser("apistream")
|
|
45
|
+
p_stream.add_argument("--echo", action="store_true")
|
|
46
|
+
p_stream.add_argument("--auto", action="store_true")
|
|
47
|
+
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
mgr = GatewayManager(args.addr, auto=getattr(args, "auto", False))
|
|
50
|
+
addr = mgr.ensure_running()
|
|
51
|
+
|
|
52
|
+
if args.cmd == "pub":
|
|
53
|
+
client.publish_data(
|
|
54
|
+
addr, topic=args.topic, attribute=args.attribute,
|
|
55
|
+
time_iso=args.time_iso, value_number=args.value,
|
|
56
|
+
uom=args.uom, data_group=args.group,
|
|
57
|
+
value_is_cumulative=args.cumulative
|
|
58
|
+
)
|
|
59
|
+
elif args.cmd == "sub":
|
|
60
|
+
client.subscribe(addr, args.topics)
|
|
61
|
+
elif args.cmd == "regapi":
|
|
62
|
+
with make_channel(addr) as ch:
|
|
63
|
+
stub = gw.UnsGatewayStub(ch)
|
|
64
|
+
client.register_api(stub, args.topic, args.attribute, args.desc, args.tag, args.param)
|
|
65
|
+
print("registered")
|
|
66
|
+
elif args.cmd == "unregapi":
|
|
67
|
+
with make_channel(addr) as ch:
|
|
68
|
+
stub = gw.UnsGatewayStub(ch)
|
|
69
|
+
client.unregister_api(stub, args.topic, args.attribute)
|
|
70
|
+
print("unregistered")
|
|
71
|
+
elif args.cmd == "apistream":
|
|
72
|
+
client.api_stream(addr, echo=args.echo)
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Iterable, Dict, Callable, List
|
|
2
|
+
import queue
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import grpc
|
|
7
|
+
from .manager import GatewayManager
|
|
8
|
+
|
|
9
|
+
from gen import uns_gateway_pb2 as pb2
|
|
10
|
+
from gen import uns_gateway_pb2_grpc as gw
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def iso_now() -> str:
|
|
14
|
+
"""Return current UTC time in ISO format with milliseconds."""
|
|
15
|
+
dt = datetime.now(timezone.utc)
|
|
16
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_channel(addr: str) -> grpc.Channel:
|
|
20
|
+
"""Create a gRPC insecure channel."""
|
|
21
|
+
return grpc.insecure_channel(addr)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Client:
|
|
25
|
+
"""
|
|
26
|
+
High-level client for UNS gateway.
|
|
27
|
+
Automatically manages gateway lifecycle and keeps a persistent stub.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, addr: str | None = None, auto: bool = True, timeout_s: int = 20):
|
|
31
|
+
self.manager = GatewayManager(addr=addr, auto=auto, timeout_s=timeout_s)
|
|
32
|
+
self.addr = self.manager.ensure_running()
|
|
33
|
+
self._ch = make_channel(self.addr)
|
|
34
|
+
self._stub = gw.UnsGatewayStub(self._ch)
|
|
35
|
+
|
|
36
|
+
# --- Publish ---
|
|
37
|
+
def publish_data(self, topic: str, attribute: str, value: float,
|
|
38
|
+
time_iso: str | None = None, uom: str = "",
|
|
39
|
+
data_group: str = "", cumulative: bool = False):
|
|
40
|
+
time_iso = time_iso or iso_now()
|
|
41
|
+
req = pb2.PublishRequest(
|
|
42
|
+
topic=topic,
|
|
43
|
+
attribute=attribute,
|
|
44
|
+
data=pb2.Data(
|
|
45
|
+
time=time_iso,
|
|
46
|
+
value_number=value,
|
|
47
|
+
uom=uom,
|
|
48
|
+
data_group=data_group
|
|
49
|
+
),
|
|
50
|
+
value_is_cumulative=cumulative
|
|
51
|
+
)
|
|
52
|
+
res = self._stub.Publish(req)
|
|
53
|
+
if not res.ok:
|
|
54
|
+
raise RuntimeError(res.error)
|
|
55
|
+
|
|
56
|
+
def publish_table(self, topic: str, attribute: str, obj: Dict[str, any],
|
|
57
|
+
time_iso: str | None = None, data_group: str = "default"):
|
|
58
|
+
"""
|
|
59
|
+
Publish a dictionary as a Table.
|
|
60
|
+
Numeric values -> value_number
|
|
61
|
+
None -> empty
|
|
62
|
+
Other -> value_string
|
|
63
|
+
"""
|
|
64
|
+
time_iso = time_iso or iso_now()
|
|
65
|
+
tv_list = []
|
|
66
|
+
for k, v in obj.items():
|
|
67
|
+
if isinstance(v, (int, float)):
|
|
68
|
+
tv_list.append(pb2.TableValue(key=k, value_number=float(v)))
|
|
69
|
+
elif v is None:
|
|
70
|
+
tv_list.append(pb2.TableValue(key=k))
|
|
71
|
+
else:
|
|
72
|
+
tv_list.append(pb2.TableValue(key=k, value_string=str(v)))
|
|
73
|
+
|
|
74
|
+
req = pb2.PublishRequest(
|
|
75
|
+
topic=topic,
|
|
76
|
+
attribute=attribute,
|
|
77
|
+
table=pb2.Table(time=time_iso, values=tv_list, data_group=data_group)
|
|
78
|
+
)
|
|
79
|
+
res = self._stub.Publish(req)
|
|
80
|
+
if not res.ok:
|
|
81
|
+
raise RuntimeError(res.error)
|
|
82
|
+
# --- Subscribe ---
|
|
83
|
+
def subscribe(self, topics: Iterable[str], callback: Callable[[any], None] | None = None):
|
|
84
|
+
"""
|
|
85
|
+
Subscribe to topics and call the provided callback on each message.
|
|
86
|
+
"""
|
|
87
|
+
stub = self._stub
|
|
88
|
+
try:
|
|
89
|
+
stub.Ready(pb2.ReadyRequest(timeout_ms=15000, wait_input=True))
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
stream = stub.Subscribe(pb2.SubscribeRequest(topics=list(topics)))
|
|
94
|
+
try:
|
|
95
|
+
for msg in stream:
|
|
96
|
+
if callback:
|
|
97
|
+
callback(msg)
|
|
98
|
+
else:
|
|
99
|
+
print(f"{msg.topic}: {msg.payload}")
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# --- API Register ---
|
|
104
|
+
def register_api(self, topic: str, attribute: str, desc: str = "",
|
|
105
|
+
tags: list[str] | None = None,
|
|
106
|
+
query_params: List[pb2.ApiQueryParam] | None = None):
|
|
107
|
+
tags = tags or []
|
|
108
|
+
query_params = query_params or []
|
|
109
|
+
|
|
110
|
+
res = self._stub.RegisterApiGet(
|
|
111
|
+
pb2.RegisterApiGetRequest(
|
|
112
|
+
topic=topic,
|
|
113
|
+
attribute=attribute,
|
|
114
|
+
api_description=desc,
|
|
115
|
+
tags=tags,
|
|
116
|
+
query_params=query_params
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
if not res.ok:
|
|
120
|
+
raise RuntimeError(f"Register failed: {res.error}")
|
|
121
|
+
|
|
122
|
+
def unregister_api(self, topic: str, attribute: str):
|
|
123
|
+
res = self._stub.UnregisterApiGet(
|
|
124
|
+
pb2.UnregisterApiGetRequest(topic=topic, attribute=attribute)
|
|
125
|
+
)
|
|
126
|
+
if not res.ok:
|
|
127
|
+
raise RuntimeError(f"Unregister failed: {res.error}")
|
|
128
|
+
|
|
129
|
+
# --- API Stream ---
|
|
130
|
+
def api_stream(self, echo: bool = False, handle_event: Callable | None = None):
|
|
131
|
+
q: "queue.Queue[pb2.ApiEventResponse|None]" = queue.Queue()
|
|
132
|
+
|
|
133
|
+
def req_iter():
|
|
134
|
+
while True:
|
|
135
|
+
item = q.get()
|
|
136
|
+
if item is None:
|
|
137
|
+
break
|
|
138
|
+
yield item
|
|
139
|
+
|
|
140
|
+
stream = self._stub.ApiEventStream(req_iter())
|
|
141
|
+
try:
|
|
142
|
+
for ev in stream:
|
|
143
|
+
if handle_event:
|
|
144
|
+
resp = handle_event(ev)
|
|
145
|
+
if resp:
|
|
146
|
+
q.put(resp)
|
|
147
|
+
continue
|
|
148
|
+
if echo:
|
|
149
|
+
q.put(pb2.ApiEventResponse(id=ev.id, status=200, body="OK"))
|
|
150
|
+
continue
|
|
151
|
+
body = {"status": "OK", "endpoint": ev.path, "query": dict(ev.query)}
|
|
152
|
+
q.put(pb2.ApiEventResponse(id=ev.id, status=200, headers={"Content-Type": "application/json"},
|
|
153
|
+
body=json.dumps(body)))
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
q.put(None)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import atexit
|
|
7
|
+
import signal
|
|
8
|
+
import grpc
|
|
9
|
+
|
|
10
|
+
class GatewayManager:
|
|
11
|
+
"""
|
|
12
|
+
Ensures a Node.js UNS gateway is running.
|
|
13
|
+
Can start it automatically if needed.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, addr: str | None = None, auto: bool = True, timeout_s: int = 20):
|
|
16
|
+
self.addr = addr
|
|
17
|
+
self.auto = auto
|
|
18
|
+
self.timeout_s = timeout_s
|
|
19
|
+
self._proc: subprocess.Popen | None = None
|
|
20
|
+
|
|
21
|
+
def _default_addr(self) -> str:
|
|
22
|
+
if os.name == "nt":
|
|
23
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
24
|
+
s.bind(("127.0.0.1", 0))
|
|
25
|
+
port = s.getsockname()[1]
|
|
26
|
+
return f"127.0.0.1:{port}"
|
|
27
|
+
else:
|
|
28
|
+
script = os.path.basename(sys.argv[0]).replace(".py", "")
|
|
29
|
+
return f"unix:/tmp/uns-gateway-{script}-{os.getpid()}.sock"
|
|
30
|
+
|
|
31
|
+
def _cleanup(self):
|
|
32
|
+
if not self._proc:
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
if self._proc.poll() is None:
|
|
36
|
+
if os.name == "nt":
|
|
37
|
+
self._proc.terminate()
|
|
38
|
+
else:
|
|
39
|
+
os.killpg(os.getpgid(self._proc.pid), signal.SIGTERM)
|
|
40
|
+
try:
|
|
41
|
+
self._proc.wait(timeout=3)
|
|
42
|
+
except Exception:
|
|
43
|
+
if os.name == "nt":
|
|
44
|
+
self._proc.kill()
|
|
45
|
+
else:
|
|
46
|
+
os.killpg(os.getpgid(self._proc.pid), signal.SIGKILL)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
self._proc = None
|
|
50
|
+
|
|
51
|
+
def ensure_running(self) -> str:
|
|
52
|
+
addr = self.addr or self._default_addr()
|
|
53
|
+
ch = grpc.insecure_channel(addr)
|
|
54
|
+
try:
|
|
55
|
+
grpc.channel_ready_future(ch).result(timeout=2)
|
|
56
|
+
ch.close()
|
|
57
|
+
return addr
|
|
58
|
+
except Exception:
|
|
59
|
+
ch.close()
|
|
60
|
+
if not self.auto:
|
|
61
|
+
return addr
|
|
62
|
+
|
|
63
|
+
# Spawn Node.js gateway
|
|
64
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
65
|
+
cli_path = os.path.join(repo_root, "node_modules", "@uns-kit", "core", "dist", "uns-grpc", "uns-gateway-cli")
|
|
66
|
+
popen_kwargs = {}
|
|
67
|
+
creationflags = 0
|
|
68
|
+
if os.name != "nt":
|
|
69
|
+
popen_kwargs["preexec_fn"] = os.setsid
|
|
70
|
+
else:
|
|
71
|
+
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
72
|
+
|
|
73
|
+
suffix = f"py-{os.path.basename(sys.argv[0]).replace('.py','')}-{os.getpid()}"
|
|
74
|
+
self._proc = subprocess.Popen(
|
|
75
|
+
["node", cli_path, "--addr", addr, "--instanceSuffix", suffix, "--instanceMode", "force"],
|
|
76
|
+
cwd=repo_root,
|
|
77
|
+
creationflags=creationflags,
|
|
78
|
+
**popen_kwargs,
|
|
79
|
+
)
|
|
80
|
+
atexit.register(self._cleanup)
|
|
81
|
+
|
|
82
|
+
# Wait for channel ready
|
|
83
|
+
start = time.time()
|
|
84
|
+
while time.time() - start < self.timeout_s:
|
|
85
|
+
ch2 = grpc.insecure_channel(addr)
|
|
86
|
+
try:
|
|
87
|
+
grpc.channel_ready_future(ch2).result(timeout=2)
|
|
88
|
+
ch2.close()
|
|
89
|
+
wait_s = int(os.environ.get("UNS_GATEWAY_HANDOVER_WAIT", "11"))
|
|
90
|
+
if wait_s > 0:
|
|
91
|
+
time.sleep(wait_s)
|
|
92
|
+
return addr
|
|
93
|
+
except Exception:
|
|
94
|
+
ch2.close()
|
|
95
|
+
time.sleep(0.5)
|
|
96
|
+
|
|
97
|
+
raise RuntimeError("Gateway did not become ready in time")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"Main Logic"
|
|
@@ -5,7 +5,7 @@ set -euo pipefail
|
|
|
5
5
|
# Navigate to the project root
|
|
6
6
|
cd "$(dirname "$0")/.."
|
|
7
7
|
|
|
8
|
-
# Detect OS and set
|
|
8
|
+
# Detect OS and set paths
|
|
9
9
|
PLATFORM="$(uname)"
|
|
10
10
|
if [[ "$PLATFORM" == "Linux" || "$PLATFORM" == "Darwin" ]]; then
|
|
11
11
|
PYTHON_EXEC="python3"
|
|
@@ -20,16 +20,23 @@ else
|
|
|
20
20
|
exit 1
|
|
21
21
|
fi
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
if [[ "$PLATFORM" =~ MINGW.*|MSYS.*|CYGWIN.*|Windows_NT ]]; then
|
|
24
|
+
UV_CMD="$HOME/.local/bin/uv.exe"
|
|
25
|
+
else
|
|
26
|
+
UV_CMD="$HOME/.cargo/bin/uv"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$UV_CMD" ]; then
|
|
30
|
+
echo "[setup] uv not found. Installing it..."
|
|
31
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
32
|
+
else
|
|
33
|
+
echo "[setup] uv already installed at $UV_CMD"
|
|
27
34
|
fi
|
|
28
35
|
|
|
29
|
-
# Create virtual environment
|
|
36
|
+
# Create virtual environment using uv
|
|
30
37
|
if [ ! -d "$VENV_DIR" ]; then
|
|
31
|
-
echo "[setup] Creating virtual environment in $VENV_DIR..."
|
|
32
|
-
"$
|
|
38
|
+
echo "[setup] Creating virtual environment in $VENV_DIR using uv..."
|
|
39
|
+
"$UV_CMD" venv "$VENV_DIR"
|
|
33
40
|
fi
|
|
34
41
|
|
|
35
42
|
# Create gen directory if it doesn't exist
|
|
@@ -46,14 +53,19 @@ fi
|
|
|
46
53
|
|
|
47
54
|
# Activate the virtual environment
|
|
48
55
|
echo "[setup] Activating virtual environment..."
|
|
49
|
-
|
|
56
|
+
if [[ "$PLATFORM" =~ MINGW.*|MSYS.*|CYGWIN.* ]]; then
|
|
57
|
+
# Windows Git Bash / MSYS
|
|
58
|
+
. "$VENV_ACTIVATE"
|
|
59
|
+
else
|
|
60
|
+
# Linux / macOS
|
|
61
|
+
source "$VENV_ACTIVATE"
|
|
62
|
+
fi
|
|
50
63
|
|
|
51
|
-
# Install
|
|
52
|
-
echo "[setup] Installing requirements using
|
|
53
|
-
"$
|
|
54
|
-
"$PYTHON_EXEC" -m pip install -r requirements.txt
|
|
64
|
+
# Install dependencies using uv (much faster than pip)
|
|
65
|
+
echo "[setup] Installing requirements using uv..."
|
|
66
|
+
"$UV_CMD" sync --active
|
|
55
67
|
|
|
56
|
-
# Generate Python gRPC code from
|
|
68
|
+
# Generate Python gRPC code from proto
|
|
57
69
|
echo "[setup] Generating gRPC Python code..."
|
|
58
70
|
"$PYTHON_EXEC" -m grpc_tools.protoc \
|
|
59
71
|
-I=proto \
|
|
@@ -61,4 +73,15 @@ echo "[setup] Generating gRPC Python code..."
|
|
|
61
73
|
--grpc_python_out=gen \
|
|
62
74
|
proto/uns-gateway.proto
|
|
63
75
|
|
|
76
|
+
# Patch the generated _pb2_grpc.py to use relative imports
|
|
77
|
+
GRPC_FILE="gen/uns_gateway_pb2_grpc.py"
|
|
78
|
+
if [[ -f "$GRPC_FILE" ]]; then
|
|
79
|
+
echo "[setup] Patching $GRPC_FILE to use relative imports..."
|
|
80
|
+
if [[ "$PLATFORM" == "Linux" || "$PLATFORM" == "Darwin" ]]; then
|
|
81
|
+
sed -i 's/^import \(.*_pb2\) as/from . import \1 as/' "$GRPC_FILE"
|
|
82
|
+
elif [[ "$PLATFORM" =~ MINGW.*|MSYS.*|CYGWIN.* ]]; then
|
|
83
|
+
sed -i'' 's/^import \(.*_pb2\) as/from . import \1 as/' "$GRPC_FILE"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
64
87
|
echo "[setup] Setup complete!"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { UnsProxyProcess, ConfigFile, logger } from "@uns-kit/core";
|
|
5
5
|
import { ITemporalTopic } from "@uns-kit/temporal";
|
|
6
|
+
import "@uns-kit/temporal";
|
|
6
7
|
import { type UnsProxyProcessWithTemporal } from "@uns-kit/temporal";
|
|
7
8
|
import { UnsAttributeType } from "@uns-kit/core/graphql/schema.js";
|
|
8
9
|
|