nexgenpy 1.0.0__py3-none-any.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.
- nexgenpy/__init__.py +2 -0
- nexgenpy/actuators.py +185 -0
- nexgenpy/manager.py +644 -0
- nexgenpy-1.0.0.dist-info/METADATA +239 -0
- nexgenpy-1.0.0.dist-info/RECORD +7 -0
- nexgenpy-1.0.0.dist-info/WHEEL +5 -0
- nexgenpy-1.0.0.dist-info/top_level.txt +1 -0
nexgenpy/__init__.py
ADDED
nexgenpy/actuators.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""# Actuators.
|
|
2
|
+
|
|
3
|
+
It contains the decorators for the actuators.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import textwrap
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Any, Callable, Dict, List, Literal, get_type_hints
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE_URL = "http://localhost:8000/api/v1"
|
|
19
|
+
DEFAULT_TIMEOUT = 60
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _function_source(func: Callable[..., Any]) -> str:
|
|
23
|
+
"""# Function Source.
|
|
24
|
+
|
|
25
|
+
It returns the source code of the function without its decorators, so the
|
|
26
|
+
downloaded method.py does not need to import them.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
func (Callable[..., Any]): The decorated function.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: The source code that starts at the `def` line.
|
|
33
|
+
"""
|
|
34
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
35
|
+
node = ast.parse(source).body[0]
|
|
36
|
+
lines = source.splitlines(keepends=True)
|
|
37
|
+
return "".join(lines[node.lineno - 1 :])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _request_timeout() -> float:
|
|
41
|
+
"""# Request Timeout.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
float: The value of NEXGEN_TIMEOUT, or the default if it is not valid.
|
|
45
|
+
"""
|
|
46
|
+
raw_timeout = os.getenv("NEXGEN_TIMEOUT")
|
|
47
|
+
if raw_timeout is None:
|
|
48
|
+
return DEFAULT_TIMEOUT
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
timeout = float(raw_timeout)
|
|
52
|
+
except ValueError:
|
|
53
|
+
timeout = 0
|
|
54
|
+
|
|
55
|
+
if timeout <= 0:
|
|
56
|
+
logger.warning(
|
|
57
|
+
"NEXGEN_TIMEOUT is invalid: %r, using %d",
|
|
58
|
+
raw_timeout,
|
|
59
|
+
DEFAULT_TIMEOUT,
|
|
60
|
+
)
|
|
61
|
+
return DEFAULT_TIMEOUT
|
|
62
|
+
|
|
63
|
+
return timeout
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def actuator(
|
|
67
|
+
name: str,
|
|
68
|
+
atype: Literal["coding", "decoding"],
|
|
69
|
+
version: str,
|
|
70
|
+
is_latest: bool,
|
|
71
|
+
requirements: List[str] | None,
|
|
72
|
+
) -> Callable[[Callable[..., Any]], Callable[..., bool]]:
|
|
73
|
+
"""# Actuator.
|
|
74
|
+
|
|
75
|
+
It holds the information for the actuator. A call to the decorated function
|
|
76
|
+
publishes the actuator to the Nexgen API.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name (str): Name of the actuator.
|
|
80
|
+
atype (Literal["coding", "decoding"]): Type of the actuator.
|
|
81
|
+
version (str): Version of the actuator.
|
|
82
|
+
is_latest (bool): If it is true, the server marks this version as latest.
|
|
83
|
+
requirements (List[str] | None): Pip requirements of the actuator.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Callable[[Callable[..., Any]], Callable[..., bool]]: The decorator.
|
|
87
|
+
"""
|
|
88
|
+
# Checking The Datatypes
|
|
89
|
+
if isinstance(name, str) is False:
|
|
90
|
+
logger.error("name is invalid, it has to be str")
|
|
91
|
+
raise ValueError("name is invalid, it has to be str")
|
|
92
|
+
|
|
93
|
+
if atype not in ["coding", "decoding"]:
|
|
94
|
+
logger.error("atype is invalid, it has to be either coding or decoding")
|
|
95
|
+
raise ValueError("atype is invalid, it has to be either coding or decoding")
|
|
96
|
+
|
|
97
|
+
if isinstance(version, str) is False:
|
|
98
|
+
logger.error("version is invalid, it has to be str")
|
|
99
|
+
raise ValueError("version is invalid, it has to be str")
|
|
100
|
+
|
|
101
|
+
if isinstance(is_latest, bool) is False:
|
|
102
|
+
logger.error("is_latest is invalid, it has to be bool")
|
|
103
|
+
raise ValueError("is_latest is invalid, it has to be bool")
|
|
104
|
+
|
|
105
|
+
if requirements is not None and (
|
|
106
|
+
isinstance(requirements, list) is False
|
|
107
|
+
or all(isinstance(r, str) for r in requirements) is False
|
|
108
|
+
):
|
|
109
|
+
logger.error("requirements is invalid, it has to be List[str]")
|
|
110
|
+
raise ValueError("requirements is invalid, it has to be List[str]")
|
|
111
|
+
|
|
112
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., bool]:
|
|
113
|
+
"""# Decorator Method."""
|
|
114
|
+
signature = inspect.signature(func)
|
|
115
|
+
type_hints = get_type_hints(func)
|
|
116
|
+
|
|
117
|
+
arguments: Dict[str, Dict[str, str]] = {}
|
|
118
|
+
|
|
119
|
+
# The server accepts only str values, so missing info is left out
|
|
120
|
+
for parameter_name, parameter in signature.parameters.items():
|
|
121
|
+
annotation = type_hints.get(parameter_name)
|
|
122
|
+
argument: Dict[str, str] = {}
|
|
123
|
+
|
|
124
|
+
if annotation is not None:
|
|
125
|
+
argument["type"] = str(annotation)
|
|
126
|
+
|
|
127
|
+
if parameter.default is not inspect.Parameter.empty:
|
|
128
|
+
argument["default"] = str(parameter.default)
|
|
129
|
+
|
|
130
|
+
arguments[parameter_name] = argument
|
|
131
|
+
|
|
132
|
+
sample: Dict[str, Any] = {
|
|
133
|
+
"name": name,
|
|
134
|
+
"version": version,
|
|
135
|
+
"actuator_type": atype,
|
|
136
|
+
"set_latest": is_latest,
|
|
137
|
+
"requirements": requirements if requirements is not None else [],
|
|
138
|
+
"method": _function_source(func),
|
|
139
|
+
"method_args": arguments,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@wraps(func)
|
|
143
|
+
def wrapper(*args: Any, **kwargs: Any) -> bool: # noqa: ANN401, ARG001
|
|
144
|
+
"""# Wrapper Method.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
bool: True if the server accepted the actuator, else False.
|
|
148
|
+
"""
|
|
149
|
+
base_url = os.getenv("NEXGEN_BASE_URL", DEFAULT_BASE_URL)
|
|
150
|
+
|
|
151
|
+
api_key = os.getenv("NEXGEN_API_KEY")
|
|
152
|
+
headers: Dict[str, str] = {}
|
|
153
|
+
|
|
154
|
+
if api_key is not None:
|
|
155
|
+
headers["X-API-KEY"] = api_key
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
result = requests.post(
|
|
159
|
+
f"{base_url}/actuators",
|
|
160
|
+
headers=headers,
|
|
161
|
+
json=sample,
|
|
162
|
+
timeout=_request_timeout(),
|
|
163
|
+
)
|
|
164
|
+
except requests.RequestException as e:
|
|
165
|
+
logger.warning("POST /actuators failed: %s", e)
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
if result.ok is True:
|
|
169
|
+
logger.info(
|
|
170
|
+
"POST /actuators succeeded: status=%d, response=%s",
|
|
171
|
+
result.status_code,
|
|
172
|
+
result.text,
|
|
173
|
+
)
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
logger.warning(
|
|
177
|
+
"POST /actuators failed: status=%d, response=%s",
|
|
178
|
+
result.status_code,
|
|
179
|
+
result.text,
|
|
180
|
+
)
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
return wrapper
|
|
184
|
+
|
|
185
|
+
return decorator
|
nexgenpy/manager.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""# Manager.
|
|
2
|
+
|
|
3
|
+
It manages the essential works for the actuators.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib.util
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "http://localhost:8000/api/v1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Actuator:
|
|
28
|
+
"""# Actuator.
|
|
29
|
+
|
|
30
|
+
It runs the actuator like run_actuator. It loads the method only once, in the
|
|
31
|
+
constructor, so the next runs do not import method.py again.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name (str): Name of the actuator.
|
|
35
|
+
actuators_dir (str): Located directory for the actuators.
|
|
36
|
+
dst (str | None): It pushes the data given endpoint if they have an
|
|
37
|
+
nexgen compatible endpoint.
|
|
38
|
+
dst_timeout (int): Timeout for the sending destination endpoint.
|
|
39
|
+
dst_headers (Dict[str, Any]): Required headers for the destination
|
|
40
|
+
endpoint.
|
|
41
|
+
dst_save_original (bool): It saves the original data too with lazy upload.
|
|
42
|
+
actuator_id (str | None): The server id of the actuator. It is None if dst
|
|
43
|
+
is None.
|
|
44
|
+
method (Callable[..., Any]): The function inside method.py.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
name: str,
|
|
50
|
+
actuators_dir: str = "nexgen_actuators",
|
|
51
|
+
dst: str | None = None,
|
|
52
|
+
dst_timeout: int = 60,
|
|
53
|
+
dst_headers: Dict[str, Any] | None = None,
|
|
54
|
+
dst_save_original: bool = True,
|
|
55
|
+
try_load_actuator: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
# Checking The Datatypes
|
|
58
|
+
if isinstance(name, str) is False:
|
|
59
|
+
logger.error("name is invalid, it has to be str")
|
|
60
|
+
raise ValueError("name is invalid, it has to be str")
|
|
61
|
+
|
|
62
|
+
if isinstance(actuators_dir, str) is False:
|
|
63
|
+
logger.error("actuators_dir is invalid, it has to be str")
|
|
64
|
+
raise ValueError("actuators_dir is invalid, it has to be str")
|
|
65
|
+
|
|
66
|
+
if dst is not None and isinstance(dst, str) is False:
|
|
67
|
+
logger.error("dst is invalid, it has to be str")
|
|
68
|
+
raise ValueError("dst is invalid, it has to be str")
|
|
69
|
+
|
|
70
|
+
if isinstance(dst_timeout, int) is False:
|
|
71
|
+
logger.error("dst_timeout is invalid, it has to be int")
|
|
72
|
+
raise ValueError("dst_timeout is invalid, it has to be int")
|
|
73
|
+
|
|
74
|
+
if dst_headers is not None and isinstance(dst_headers, dict) is False:
|
|
75
|
+
logger.error("dst_headers is invalid, it has to be Dict[str, Any]")
|
|
76
|
+
raise ValueError("dst_headers is invalid, it has to be Dict[str, Any]")
|
|
77
|
+
|
|
78
|
+
if isinstance(dst_save_original, bool) is False:
|
|
79
|
+
logger.error("dst_save_original is invalid, it has to be bool")
|
|
80
|
+
raise ValueError("dst_save_original is invalid, it has to be bool")
|
|
81
|
+
|
|
82
|
+
if isinstance(try_load_actuator, bool) is False:
|
|
83
|
+
logger.error("try_load_actuator is invalid, it has to be bool")
|
|
84
|
+
raise ValueError("try_load_actuator is invalid, it has to be bool")
|
|
85
|
+
|
|
86
|
+
if Path(f"{actuators_dir}/{name}").exists() is False:
|
|
87
|
+
logger.warning(f"Actuator: {name} does not exist")
|
|
88
|
+
if try_load_actuator is False:
|
|
89
|
+
logger.error(f"Actuator {name} has to be downloaded first")
|
|
90
|
+
raise ValueError(f"Actuator {name} has to be downloaded first")
|
|
91
|
+
else:
|
|
92
|
+
load_actuator(
|
|
93
|
+
name=name,
|
|
94
|
+
actuators_dir=actuators_dir,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
file_path = Path(f"{actuators_dir}/{name}/method.py").resolve()
|
|
98
|
+
|
|
99
|
+
spec = importlib.util.spec_from_file_location(
|
|
100
|
+
file_path.stem,
|
|
101
|
+
file_path,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if spec is None or spec.loader is None:
|
|
105
|
+
raise ImportError(f"Cannot load module: {file_path}")
|
|
106
|
+
|
|
107
|
+
module = importlib.util.module_from_spec(spec)
|
|
108
|
+
spec.loader.exec_module(module)
|
|
109
|
+
|
|
110
|
+
# Find functions defined in this file
|
|
111
|
+
methods = [
|
|
112
|
+
obj
|
|
113
|
+
for _, obj in inspect.getmembers(module, inspect.isfunction)
|
|
114
|
+
if obj.__module__ == module.__name__
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if len(methods) != 1:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Expected exactly one function, found {len(methods)}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
actuator_id = None
|
|
123
|
+
if dst is not None:
|
|
124
|
+
with open(f"{actuators_dir}/{name}/card.json", "r") as f:
|
|
125
|
+
actuator_id = json.load(f)["id"]
|
|
126
|
+
|
|
127
|
+
self.name = name
|
|
128
|
+
self.actuators_dir = actuators_dir
|
|
129
|
+
self.dst = dst
|
|
130
|
+
self.dst_timeout = dst_timeout
|
|
131
|
+
self.dst_headers = dst_headers if dst_headers is not None else {}
|
|
132
|
+
self.dst_save_original = dst_save_original
|
|
133
|
+
self.actuator_id = actuator_id
|
|
134
|
+
self.method = methods[0]
|
|
135
|
+
|
|
136
|
+
def run(
|
|
137
|
+
self,
|
|
138
|
+
data: Dict[str, Any],
|
|
139
|
+
data_specs: Dict[str, Any] | None = None,
|
|
140
|
+
) -> object:
|
|
141
|
+
"""# Run.
|
|
142
|
+
|
|
143
|
+
It runs the loaded method with the data. If dst is given, it also sends
|
|
144
|
+
the output to the destination endpoint.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
data (Dict[str, Any]): Input data for the actuator.
|
|
148
|
+
data_specs (Dict[str, Any] | None, optional): Contains the extra
|
|
149
|
+
information about the data.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
object: The output coming from actuator.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If data or data_specs is invalid.
|
|
156
|
+
"""
|
|
157
|
+
# Checking The Datatypes
|
|
158
|
+
if isinstance(data, dict) is False:
|
|
159
|
+
logger.error("data is invalid, it has to be Dict[str, Any]")
|
|
160
|
+
raise ValueError("data is invalid, it has to be Dict[str, Any]")
|
|
161
|
+
|
|
162
|
+
if data_specs is not None and isinstance(data_specs, dict) is False:
|
|
163
|
+
logger.error("data_specs is invalid, it has to be Dict[str, Any]")
|
|
164
|
+
raise ValueError("data_specs is invalid, it has to be Dict[str, Any]")
|
|
165
|
+
|
|
166
|
+
output = self.method(**data)
|
|
167
|
+
|
|
168
|
+
if self.dst is None:
|
|
169
|
+
return output
|
|
170
|
+
|
|
171
|
+
linked_resource: str | None = None
|
|
172
|
+
if self.dst_save_original is True:
|
|
173
|
+
linked_resource = str(uuid4())
|
|
174
|
+
|
|
175
|
+
# Lazy upload, so the main process does not wait for it
|
|
176
|
+
threading.Thread(
|
|
177
|
+
target=_send_content,
|
|
178
|
+
kwargs={
|
|
179
|
+
"url": f"{self.dst}/contents",
|
|
180
|
+
"headers": self.dst_headers,
|
|
181
|
+
"timeout": self.dst_timeout,
|
|
182
|
+
"identifier": linked_resource,
|
|
183
|
+
"specs": data,
|
|
184
|
+
},
|
|
185
|
+
).start()
|
|
186
|
+
|
|
187
|
+
result = requests.post(
|
|
188
|
+
url=f"{self.dst}/data",
|
|
189
|
+
headers=self.dst_headers,
|
|
190
|
+
json={
|
|
191
|
+
"actuator_id": self.actuator_id,
|
|
192
|
+
"metadata": output,
|
|
193
|
+
"linked_resource": linked_resource,
|
|
194
|
+
"specs": data_specs,
|
|
195
|
+
},
|
|
196
|
+
timeout=self.dst_timeout,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if result.ok is True:
|
|
200
|
+
logger.info(
|
|
201
|
+
"POST /data succeeded: status=%d, response=%s",
|
|
202
|
+
result.status_code,
|
|
203
|
+
result.text,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
logger.warning(
|
|
207
|
+
"POST /data failed: status=%d, response=%s",
|
|
208
|
+
result.status_code,
|
|
209
|
+
result.text,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return output
|
|
213
|
+
|
|
214
|
+
async def arun(
|
|
215
|
+
self,
|
|
216
|
+
data: Dict[str, Any],
|
|
217
|
+
data_specs: Dict[str, Any] | None = None,
|
|
218
|
+
) -> object:
|
|
219
|
+
"""# Async Run.
|
|
220
|
+
|
|
221
|
+
It is the async version of run. It runs the loaded method and the POST
|
|
222
|
+
/data request in a worker thread, so they do not block the event loop.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data (Dict[str, Any]): Input data for the actuator.
|
|
226
|
+
data_specs (Dict[str, Any] | None, optional): Contains the extra
|
|
227
|
+
information about the data.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
object: The output coming from actuator.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ValueError: If data or data_specs is invalid.
|
|
234
|
+
"""
|
|
235
|
+
# Checking The Datatypes
|
|
236
|
+
if isinstance(data, dict) is False:
|
|
237
|
+
logger.error("data is invalid, it has to be Dict[str, Any]")
|
|
238
|
+
raise ValueError("data is invalid, it has to be Dict[str, Any]")
|
|
239
|
+
|
|
240
|
+
if data_specs is not None and isinstance(data_specs, dict) is False:
|
|
241
|
+
logger.error("data_specs is invalid, it has to be Dict[str, Any]")
|
|
242
|
+
raise ValueError("data_specs is invalid, it has to be Dict[str, Any]")
|
|
243
|
+
|
|
244
|
+
output = await asyncio.to_thread(self.method, **data)
|
|
245
|
+
|
|
246
|
+
if self.dst is None:
|
|
247
|
+
return output
|
|
248
|
+
|
|
249
|
+
linked_resource: str | None = None
|
|
250
|
+
if self.dst_save_original is True:
|
|
251
|
+
linked_resource = str(uuid4())
|
|
252
|
+
|
|
253
|
+
# Lazy upload, so the main process does not wait for it
|
|
254
|
+
threading.Thread(
|
|
255
|
+
target=_send_content,
|
|
256
|
+
kwargs={
|
|
257
|
+
"url": f"{self.dst}/contents",
|
|
258
|
+
"headers": self.dst_headers,
|
|
259
|
+
"timeout": self.dst_timeout,
|
|
260
|
+
"identifier": linked_resource,
|
|
261
|
+
"specs": data,
|
|
262
|
+
},
|
|
263
|
+
).start()
|
|
264
|
+
|
|
265
|
+
result = await asyncio.to_thread(
|
|
266
|
+
requests.post,
|
|
267
|
+
url=f"{self.dst}/data",
|
|
268
|
+
headers=self.dst_headers,
|
|
269
|
+
json={
|
|
270
|
+
"actuator_id": self.actuator_id,
|
|
271
|
+
"metadata": output,
|
|
272
|
+
"linked_resource": linked_resource,
|
|
273
|
+
"specs": data_specs,
|
|
274
|
+
},
|
|
275
|
+
timeout=self.dst_timeout,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if result.ok is True:
|
|
279
|
+
logger.info(
|
|
280
|
+
"POST /data succeeded: status=%d, response=%s",
|
|
281
|
+
result.status_code,
|
|
282
|
+
result.text,
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
logger.warning(
|
|
286
|
+
"POST /data failed: status=%d, response=%s",
|
|
287
|
+
result.status_code,
|
|
288
|
+
result.text,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return output
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _send_content(
|
|
295
|
+
url: str,
|
|
296
|
+
headers: Dict[str, Any],
|
|
297
|
+
timeout: int,
|
|
298
|
+
identifier: str,
|
|
299
|
+
specs: Dict[str, Any],
|
|
300
|
+
) -> None:
|
|
301
|
+
"""# Send Content.
|
|
302
|
+
|
|
303
|
+
It sends the original data to the content endpoint. It runs in a background
|
|
304
|
+
thread, so it logs the errors and does not raise them.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
url (str): The content endpoint.
|
|
308
|
+
headers (Dict[str, Any]): Headers for the content endpoint.
|
|
309
|
+
timeout (int): Timeout for the content endpoint.
|
|
310
|
+
identifier (str): The linked resource of the data.
|
|
311
|
+
specs (Dict[str, Any]): The original data.
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
result = requests.post(
|
|
315
|
+
url=url,
|
|
316
|
+
headers=headers,
|
|
317
|
+
json={
|
|
318
|
+
"identifier": identifier,
|
|
319
|
+
"specs": specs,
|
|
320
|
+
},
|
|
321
|
+
timeout=timeout,
|
|
322
|
+
)
|
|
323
|
+
except requests.RequestException as e:
|
|
324
|
+
logger.warning("POST /contents failed: %s", e)
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
if result.ok is True:
|
|
328
|
+
logger.info(
|
|
329
|
+
"POST /contents succeeded: status=%d, response=%s",
|
|
330
|
+
result.status_code,
|
|
331
|
+
result.text,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
logger.warning(
|
|
335
|
+
"POST /contents failed: status=%d, response=%s",
|
|
336
|
+
result.status_code,
|
|
337
|
+
result.text,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def run_actuator(
|
|
342
|
+
name: str,
|
|
343
|
+
data: Dict[str, Any],
|
|
344
|
+
data_specs: Dict[str, Any] | None = None,
|
|
345
|
+
actuators_dir: str = "nexgen_actuators",
|
|
346
|
+
dst: str | None = None,
|
|
347
|
+
dst_timeout: int = 60,
|
|
348
|
+
dst_headers: Dict[str, Any] | None = None,
|
|
349
|
+
dst_save_original: bool = True,
|
|
350
|
+
try_load_actuator: bool = True,
|
|
351
|
+
) -> object:
|
|
352
|
+
"""# Run Actuator.
|
|
353
|
+
|
|
354
|
+
It basically runs the actuator. It raises error if actuator is not seen.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
name (str): Name of the actuator.
|
|
358
|
+
data (Dict[str, Any]): Input data for the actuator.
|
|
359
|
+
data_specs (Dict[str, Any] | None): Contains the extra information about the
|
|
360
|
+
data.
|
|
361
|
+
actuators_dir (str, optional): Located directory for the actuators.
|
|
362
|
+
dst (str | None, optional): It pushes the data given endpoint if they have an
|
|
363
|
+
nexgen compatible endpoint.
|
|
364
|
+
dst_timeout (int, optional): Timeout for the sending destination endpoint.
|
|
365
|
+
dst_headers (Dict[str, Any] | None, optional): Required headers for the
|
|
366
|
+
destination endpoint, if needed.
|
|
367
|
+
dst_save_original (bool): It saves the original data too with lazy upload.
|
|
368
|
+
try_load_actuator (bool, optional): It tries to download the actuator first
|
|
369
|
+
if it is does not exist locally.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
object: The output coming from actuator.
|
|
373
|
+
"""
|
|
374
|
+
# Checking The Datatype
|
|
375
|
+
if isinstance(name, str) is False:
|
|
376
|
+
logger.error("name is invalid, it has to be str")
|
|
377
|
+
raise ValueError("name is invalid, it has to be str")
|
|
378
|
+
|
|
379
|
+
if isinstance(data, dict) is False:
|
|
380
|
+
logger.error("data is invalid, it has to be Dict[str, Any]")
|
|
381
|
+
raise ValueError("data is invalid, it has to be Dict[str, Any]")
|
|
382
|
+
|
|
383
|
+
if data_specs is not None and isinstance(data_specs, dict) is False:
|
|
384
|
+
logger.error("data_specs is invalid, it has to be Dict[str, Any]")
|
|
385
|
+
raise ValueError("data_specs is invalid, it has to be Dict[str, Any]")
|
|
386
|
+
|
|
387
|
+
if isinstance(actuators_dir, str) is False:
|
|
388
|
+
logger.error("actuators_dir is invalid, it has to be str")
|
|
389
|
+
raise ValueError("actuators_dir is invalid, it has to be str")
|
|
390
|
+
|
|
391
|
+
if dst is not None and isinstance(dst, str) is False:
|
|
392
|
+
logger.error("dst is invalid, it has to be str")
|
|
393
|
+
raise ValueError("dst is invalid, it has to be str")
|
|
394
|
+
|
|
395
|
+
if isinstance(dst_timeout, int) is False:
|
|
396
|
+
logger.error("dst_timeout is invalid, it has to be int")
|
|
397
|
+
raise ValueError("dst_timeout is invalid, it has to be int")
|
|
398
|
+
|
|
399
|
+
if dst_headers is not None and isinstance(dst_headers, dict) is False:
|
|
400
|
+
logger.error("dst_headers is invalid, it has to be Dict[str, Any]")
|
|
401
|
+
raise ValueError("dst_headers is invalid, it has to be Dict[str, Any]")
|
|
402
|
+
|
|
403
|
+
if isinstance(dst_save_original, bool) is False:
|
|
404
|
+
logger.error("dst_save_original is invalid, it has to be bool")
|
|
405
|
+
raise ValueError("dst_save_original is invalid, it has to be bool")
|
|
406
|
+
|
|
407
|
+
if isinstance(try_load_actuator, bool) is False:
|
|
408
|
+
logger.error("try_load_actuator is invalid, it has to be bool")
|
|
409
|
+
raise ValueError("try_load_actuator is invalid, it has to be bool")
|
|
410
|
+
|
|
411
|
+
if Path(f"{actuators_dir}/{name}").exists() is False:
|
|
412
|
+
logger.warning(f"Actuator: {name} does not exist")
|
|
413
|
+
if try_load_actuator is False:
|
|
414
|
+
logger.error(f"Actuator {name} has to be downloaded first")
|
|
415
|
+
raise ValueError(f"Actuator {name} has to be downloaded first")
|
|
416
|
+
else:
|
|
417
|
+
load_actuator(
|
|
418
|
+
name=name,
|
|
419
|
+
actuators_dir=actuators_dir,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
file_path = Path(f"{actuators_dir}/{name}/method.py").resolve()
|
|
423
|
+
|
|
424
|
+
spec = importlib.util.spec_from_file_location(
|
|
425
|
+
file_path.stem,
|
|
426
|
+
file_path,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if spec is None or spec.loader is None:
|
|
430
|
+
raise ImportError(f"Cannot load module: {file_path}")
|
|
431
|
+
|
|
432
|
+
module = importlib.util.module_from_spec(spec)
|
|
433
|
+
spec.loader.exec_module(module)
|
|
434
|
+
|
|
435
|
+
# Find functions defined in this file
|
|
436
|
+
methods = [
|
|
437
|
+
obj
|
|
438
|
+
for _, obj in inspect.getmembers(module, inspect.isfunction)
|
|
439
|
+
if obj.__module__ == module.__name__
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
if len(methods) != 1:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
f"Expected exactly one function, found {len(methods)}",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
method = methods[0]
|
|
448
|
+
output = method(**data)
|
|
449
|
+
|
|
450
|
+
if dst is None:
|
|
451
|
+
return output
|
|
452
|
+
|
|
453
|
+
if dst_headers is None:
|
|
454
|
+
dst_headers = {}
|
|
455
|
+
|
|
456
|
+
with open(f"{actuators_dir}/{name}/card.json", "r") as f:
|
|
457
|
+
actuator_id = json.load(f)["id"]
|
|
458
|
+
|
|
459
|
+
linked_resource: str | None = None
|
|
460
|
+
if dst_save_original is True:
|
|
461
|
+
linked_resource = str(uuid4())
|
|
462
|
+
|
|
463
|
+
# Lazy upload, so the main process does not wait for it
|
|
464
|
+
threading.Thread(
|
|
465
|
+
target=_send_content,
|
|
466
|
+
kwargs={
|
|
467
|
+
"url": f"{dst}/contents",
|
|
468
|
+
"headers": dst_headers,
|
|
469
|
+
"timeout": dst_timeout,
|
|
470
|
+
"identifier": linked_resource,
|
|
471
|
+
"specs": data,
|
|
472
|
+
},
|
|
473
|
+
).start()
|
|
474
|
+
|
|
475
|
+
result = requests.post(
|
|
476
|
+
url=f"{dst}/data",
|
|
477
|
+
headers=dst_headers,
|
|
478
|
+
json={
|
|
479
|
+
"actuator_id": actuator_id,
|
|
480
|
+
"metadata": output,
|
|
481
|
+
"linked_resource": linked_resource,
|
|
482
|
+
"specs": data_specs,
|
|
483
|
+
},
|
|
484
|
+
timeout=dst_timeout,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if result.ok is True:
|
|
488
|
+
logger.info(
|
|
489
|
+
"POST /data succeeded: status=%d, response=%s",
|
|
490
|
+
result.status_code,
|
|
491
|
+
result.text,
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
logger.warning(
|
|
495
|
+
"POST /data failed: status=%d, response=%s",
|
|
496
|
+
result.status_code,
|
|
497
|
+
result.text,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return output
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def load_actuator(
|
|
504
|
+
name: str,
|
|
505
|
+
actuators_dir: str = "nexgen_actuators",
|
|
506
|
+
load_timeout: int = 60,
|
|
507
|
+
update_actuator: bool = True,
|
|
508
|
+
) -> None:
|
|
509
|
+
"""# Load Actuator.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
name (str): The name of the actuator.
|
|
513
|
+
actuators_dir (str, optional): Located directory for the actuators.
|
|
514
|
+
load_timeout (int, optional): The timeout for the loading the actuator.
|
|
515
|
+
update_actuator (bool, optional): If it is true, even though the actuator exists
|
|
516
|
+
it checks that the latest version is changed and if it is changed then
|
|
517
|
+
override the existing actuator.
|
|
518
|
+
"""
|
|
519
|
+
# Checking The Datatypes
|
|
520
|
+
if isinstance(name, str) is False:
|
|
521
|
+
logger.error("name is invalid, it has to be str")
|
|
522
|
+
raise ValueError("name is invalid, it has to be str")
|
|
523
|
+
|
|
524
|
+
if isinstance(actuators_dir, str) is False:
|
|
525
|
+
logger.error("actuators_dir is invalid, it has to be str")
|
|
526
|
+
raise ValueError("actuators_dir is invalid, it has to be str")
|
|
527
|
+
|
|
528
|
+
if isinstance(load_timeout, int) is False:
|
|
529
|
+
logger.error("load_timeout is invalid, it has to be int")
|
|
530
|
+
raise ValueError("load_timeout is invalid, it has to be int")
|
|
531
|
+
|
|
532
|
+
if isinstance(update_actuator, bool) is False:
|
|
533
|
+
logger.error("update_actuator is invalid, it has to be bool")
|
|
534
|
+
raise ValueError("update_actuator is invalid, it has to be bool")
|
|
535
|
+
|
|
536
|
+
# The name has to point to a directory directly inside actuators_dir
|
|
537
|
+
actuators_path = Path(actuators_dir).resolve()
|
|
538
|
+
if (actuators_path / name).resolve().parent != actuators_path:
|
|
539
|
+
logger.error("name is invalid, it has to be a plain directory name")
|
|
540
|
+
raise ValueError("name is invalid, it has to be a plain directory name")
|
|
541
|
+
|
|
542
|
+
if update_actuator is False and Path(f"{actuators_dir}/{name}").exists() is True:
|
|
543
|
+
logger.info(f"Actuator {name} already exists")
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
headers = {}
|
|
547
|
+
if os.getenv("NEXGEN_API_KEY") is not None:
|
|
548
|
+
headers["X-API-KEY"] = os.getenv("NEXGEN_API_KEY")
|
|
549
|
+
|
|
550
|
+
result = requests.get(
|
|
551
|
+
f"{os.getenv('NEXGEN_BASE_URL', DEFAULT_BASE_URL)}/actuators/{name}/latest",
|
|
552
|
+
headers=headers,
|
|
553
|
+
timeout=load_timeout,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
result.raise_for_status()
|
|
557
|
+
|
|
558
|
+
logger.info(
|
|
559
|
+
"GET /actuators/actuator_name/latest succeeded: status=%d, response=%s",
|
|
560
|
+
result.status_code,
|
|
561
|
+
result.text,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
response = result.json()
|
|
565
|
+
|
|
566
|
+
card_path = Path(f"{actuators_dir}/{name}/card.json")
|
|
567
|
+
if card_path.exists() is True:
|
|
568
|
+
with open(card_path, "r") as f:
|
|
569
|
+
existing_card = json.load(f)
|
|
570
|
+
|
|
571
|
+
if (
|
|
572
|
+
existing_card.get("id") == response["_id"]
|
|
573
|
+
and existing_card.get("version") == response["version"]
|
|
574
|
+
):
|
|
575
|
+
logger.info(f"Actuator {name} is already up to date")
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
if Path(f"{actuators_dir}/{name}").exists():
|
|
579
|
+
logger.info("Existing directory has been found")
|
|
580
|
+
shutil.rmtree(f"{actuators_dir}/{name}")
|
|
581
|
+
logger.info("Existing directory deleted")
|
|
582
|
+
|
|
583
|
+
os.makedirs(f"{actuators_dir}/{name}")
|
|
584
|
+
logger.info(f"New directory has been created: {actuators_dir}/{name}")
|
|
585
|
+
|
|
586
|
+
card = {
|
|
587
|
+
"id": response["_id"],
|
|
588
|
+
"name": response["name"],
|
|
589
|
+
"version": response["version"],
|
|
590
|
+
"actuator_type": response["actuator_type"],
|
|
591
|
+
"method_args": response["method_args"],
|
|
592
|
+
"latest": response["latest"],
|
|
593
|
+
"created_at": response["created_at"],
|
|
594
|
+
"updated_at": response["updated_at"],
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
requirements = "\n".join(response["requirements"])
|
|
598
|
+
method = response["method"]
|
|
599
|
+
|
|
600
|
+
with open(f"{actuators_dir}/{name}/method.py", "w") as f:
|
|
601
|
+
f.write(method)
|
|
602
|
+
|
|
603
|
+
logger.info("method.py has been created")
|
|
604
|
+
|
|
605
|
+
with open(f"{actuators_dir}/{name}/requirements.txt", "w") as f:
|
|
606
|
+
f.write(requirements)
|
|
607
|
+
|
|
608
|
+
logger.info("requirements.txt has been created")
|
|
609
|
+
|
|
610
|
+
subprocess.run( # noqa: S603
|
|
611
|
+
[sys.executable, "-m", "venv", f"{actuators_dir}/{name}/.venv"],
|
|
612
|
+
check=True,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
logger.info(".venv has been created")
|
|
616
|
+
logger.info("requirements are being downloaded...")
|
|
617
|
+
|
|
618
|
+
if sys.platform == "win32":
|
|
619
|
+
pip_path = f"{actuators_dir}/{name}/.venv/Scripts/pip.exe"
|
|
620
|
+
else:
|
|
621
|
+
pip_path = f"{actuators_dir}/{name}/.venv/bin/pip"
|
|
622
|
+
|
|
623
|
+
subprocess.run( # noqa: S603
|
|
624
|
+
[
|
|
625
|
+
pip_path,
|
|
626
|
+
"install",
|
|
627
|
+
"-r",
|
|
628
|
+
f"{actuators_dir}/{name}/requirements.txt",
|
|
629
|
+
],
|
|
630
|
+
check=True,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
logger.info("requirements downloaded")
|
|
634
|
+
|
|
635
|
+
# card.json is written last, so a failed load is not seen as up to date
|
|
636
|
+
with open(f"{actuators_dir}/{name}/card.json", "w") as f:
|
|
637
|
+
json.dump(
|
|
638
|
+
card,
|
|
639
|
+
f,
|
|
640
|
+
indent=4,
|
|
641
|
+
ensure_ascii=True,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
logger.info("card.json has been created")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexgenpy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Post Shannon Communication for every day systems
|
|
5
|
+
Author-email: iamalreadynoob <sadikefe69@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: build>=1.6.1
|
|
9
|
+
Requires-Dist: pre-commit>=4.6.2
|
|
10
|
+
Requires-Dist: requests>=2.34.2
|
|
11
|
+
Requires-Dist: ruff>=0.16.10
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
# nexgenpy
|
|
16
|
+
|
|
17
|
+
**The Python client for Nexgen Server. Publish, load and run semantic communication actuators.**
|
|
18
|
+
|
|
19
|
+
[](https://github.com/iamalreadynoob/nexgen/actions/workflows/ci.yml)
|
|
20
|
+
[](https://www.python.org/downloads/)
|
|
21
|
+
[](pyproject.toml)
|
|
22
|
+
[](https://github.com/iamalreadynoob/nexgen-server)
|
|
23
|
+
[](https://github.com/astral-sh/ruff)
|
|
24
|
+
[](https://github.com/astral-sh/uv)
|
|
25
|
+
[](.pre-commit-config.yaml)
|
|
26
|
+
|
|
27
|
+
<p>
|
|
28
|
+
<a href="#introduction">Introduction</a> •
|
|
29
|
+
<a href="#setup">Setup</a> •
|
|
30
|
+
<a href="#basic-concepts">Basic concepts</a> •
|
|
31
|
+
<a href="#documentation">Documentation</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Introduction
|
|
39
|
+
|
|
40
|
+
`nexgenpy` is the Python client library for [Nexgen Server](https://github.com/iamalreadynoob/nexgen-server). Nexgen Server is a registry and data store for semantic communication (SemCom) applications.
|
|
41
|
+
|
|
42
|
+
SemCom is a communication paradigm. Its design objective is the correct delivery of the *meaning*, or of the *task-relevant content*, of a message. Its objective is not the bit-exact copy of the transmitted symbols. Thus the methods at each end of the link are as important as the link itself.
|
|
43
|
+
|
|
44
|
+
Nexgen Server keeps these methods, called **actuators**, in one place. The server does not run actuator code. `nexgenpy` gives the applications three operations:
|
|
45
|
+
|
|
46
|
+
| Operation | API | What it does |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **Register** | `@actuator(...)` | Sends the source code of a Python function to the server as a new actuator version. |
|
|
49
|
+
| **Load** | `load_actuator(...)` | Downloads the latest version of an actuator to a local directory and installs its requirements. |
|
|
50
|
+
| **Use** | `run_actuator(...)`, `Actuator(...)` | Runs the local actuator. Optionally sends the output to the server as a data item. |
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart LR
|
|
54
|
+
subgraph DEV["Developer"]
|
|
55
|
+
F["Python function<br/>+ @actuator decorator"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
NS[("Nexgen Server<br/>actuators · data · contents")]
|
|
59
|
+
|
|
60
|
+
subgraph TX["Transmitter (nexgenpy)"]
|
|
61
|
+
S["Source s"] --> CA["coding actuator"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
subgraph RX["Receiver (nexgenpy)"]
|
|
65
|
+
DA["decoding actuator"] --> O["Output ŝ<br/>reconstruction or task decision"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
F -- "1. register<br/>POST /actuators" --> NS
|
|
69
|
+
NS -. "2. load<br/>GET /actuators/{name}/latest" .-> CA
|
|
70
|
+
NS -. "2. load" .-> DA
|
|
71
|
+
CA -- "3. use: transmitted representation" --> DA
|
|
72
|
+
CA -. "data item<br/>POST /data" .-> NS
|
|
73
|
+
S -. "content item (original)<br/>POST /contents" .-> NS
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
1. A developer writes a Python function and registers it as an actuator with a name and a version.
|
|
77
|
+
2. A transmitter and a receiver each load the latest version of the actuators that they need.
|
|
78
|
+
3. Each side runs its actuator on local data. The output can go to the server as a data item. The original input can go to the server as a content item.
|
|
79
|
+
|
|
80
|
+
`nexgenpy` supports the two SemCom objectives of Nexgen Server:
|
|
81
|
+
|
|
82
|
+
- **Semantic-level communication.** The decoding actuator recreates the source, or its meaning, from the received representation.
|
|
83
|
+
- **Goal-oriented (task-oriented) communication.** The decoding actuator identifies the input or does a task with it, for example classification. It does not recreate the source.
|
|
84
|
+
|
|
85
|
+
> [!TIP]
|
|
86
|
+
> Do you use [Claude Code](https://claude.com/claude-code)? The [nexgenpy skill](https://github.com/iamalreadynoob/nexgenpy-skill) gives Claude Code the knowledge to use `nexgenpy` in your projects. Install it to get help when you register, load and use actuators.
|
|
87
|
+
|
|
88
|
+
## Setup
|
|
89
|
+
|
|
90
|
+
### Requirements
|
|
91
|
+
|
|
92
|
+
- Python 3.12 or later
|
|
93
|
+
- A running [Nexgen Server](https://github.com/iamalreadynoob/nexgen-server#setup)
|
|
94
|
+
- The API key of the server, if the server has one
|
|
95
|
+
|
|
96
|
+
### Install the library
|
|
97
|
+
|
|
98
|
+
Install from GitHub with pip:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install "git+https://github.com/iamalreadynoob/nexgen.git"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or add it to a uv project:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv add "git+https://github.com/iamalreadynoob/nexgen.git"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The repository also keeps a built wheel in `dist/`. To install a local clone for development, see [Development](docs/development.md).
|
|
111
|
+
|
|
112
|
+
### Connect to the server
|
|
113
|
+
|
|
114
|
+
Set these environment variables:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export NEXGEN_BASE_URL="http://localhost:8000/api/v1"
|
|
118
|
+
export NEXGEN_API_KEY="<your-key>"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If `NEXGEN_BASE_URL` is not set, the library uses `http://localhost:8000/api/v1`. See [Configuration](docs/configuration.md) for all settings.
|
|
122
|
+
|
|
123
|
+
### Register a first actuator
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from nexgenpy import actuator
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@actuator(
|
|
130
|
+
name="text-embedder",
|
|
131
|
+
atype="coding",
|
|
132
|
+
version="1.0.0",
|
|
133
|
+
is_latest=True,
|
|
134
|
+
requirements=["numpy"],
|
|
135
|
+
)
|
|
136
|
+
def encode(text: str) -> list[float]:
|
|
137
|
+
import numpy as np
|
|
138
|
+
|
|
139
|
+
return np.frombuffer(text.encode(), dtype=np.uint8).astype(float).tolist()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
accepted = encode() # sends POST /actuators, returns True or False
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A call to the decorated function does not run `encode`. The call registers it.
|
|
146
|
+
|
|
147
|
+
### Load and use the actuator
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from nexgenpy import load_actuator, run_actuator
|
|
151
|
+
|
|
152
|
+
load_actuator("text-embedder")
|
|
153
|
+
|
|
154
|
+
vector = run_actuator(
|
|
155
|
+
name="text-embedder",
|
|
156
|
+
data={"text": "hello"},
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`run_actuator` runs the function in the current Python process. Thus the current environment must also have the requirements of the actuator, `numpy` in this example. See [Architecture](docs/architecture.md#the-actuator-environment).
|
|
161
|
+
|
|
162
|
+
To send the output to the server as a data item, give the server URL and the API key header:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
vector = run_actuator(
|
|
166
|
+
name="text-embedder",
|
|
167
|
+
data={"text": "hello"},
|
|
168
|
+
dst="http://localhost:8000/api/v1",
|
|
169
|
+
dst_headers={"X-API-Key": "<your-key>"},
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The [Quick start guide](docs/quickstart.md) gives a full transmitter and receiver example.
|
|
174
|
+
|
|
175
|
+
## Basic concepts
|
|
176
|
+
|
|
177
|
+
| Concept | What it is | Where it lives |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| **Actuator** | A versioned, named Python function that processes data. Its type is `coding` or `decoding`. | Server: `actuators` collection. Client: `nexgen_actuators/{name}/` |
|
|
180
|
+
| **Data** | The output of an actuator, as the link carries or uses it. Each data item refers to the actuator that made it. | Server: `data` collection |
|
|
181
|
+
| **Content** | The original input of an actuator. Applications can load it again, for example to compare it with a reconstruction. | Server: `contents` collection |
|
|
182
|
+
|
|
183
|
+
### Actuators
|
|
184
|
+
|
|
185
|
+
An actuator does one processing step in a SemCom pipeline. Examples are data embedding, compression and format conversion.
|
|
186
|
+
|
|
187
|
+
- A **`coding`** actuator changes a source into a representation for transmission. It can be a semantic encoder, a channel encoder or a joint source–channel coding (JSCC) encoder.
|
|
188
|
+
- A **`decoding`** actuator changes a received representation into an output. The output is a reconstruction of the source or a task decision.
|
|
189
|
+
|
|
190
|
+
Each actuator has a `name` and a `version`. For each name, exactly one active version is the latest. `load_actuator` always gets the latest version. Thus the transmitter and the receiver can use the same version.
|
|
191
|
+
|
|
192
|
+
### The actuator function
|
|
193
|
+
|
|
194
|
+
The server stores only the source code of the function. The code that is outside the function does not go to the server. Thus an actuator function must obey these rules:
|
|
195
|
+
|
|
196
|
+
- Put all `import` statements in the function body.
|
|
197
|
+
- Do not call helper functions from the same module. Define them in the function body.
|
|
198
|
+
- Use parameter names that match the keys of the input data. `run_actuator` calls the function with `**data`.
|
|
199
|
+
- Return a value that JSON can encode, if you send the output to the server.
|
|
200
|
+
|
|
201
|
+
[Writing actuators](docs/writing-actuators.md) explains each rule.
|
|
202
|
+
|
|
203
|
+
### Local actuator directory
|
|
204
|
+
|
|
205
|
+
`load_actuator` writes each actuator to `nexgen_actuators/{name}/`:
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
nexgen_actuators/
|
|
209
|
+
└── text-embedder/
|
|
210
|
+
├── card.json # metadata, including the server id and the version
|
|
211
|
+
├── method.py # the source code of the function
|
|
212
|
+
├── requirements.txt # the pip requirements
|
|
213
|
+
└── .venv/ # a virtual environment with the requirements
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Shared rules
|
|
217
|
+
|
|
218
|
+
- All requests go to `NEXGEN_BASE_URL`, or to `dst` for data and content items.
|
|
219
|
+
- The library sends the `X-API-KEY` header when `NEXGEN_API_KEY` is set. Data and content requests use `dst_headers` instead.
|
|
220
|
+
- The server accepts a data item only from the **latest** version of an actuator. Load the actuator again after someone publishes a new version.
|
|
221
|
+
|
|
222
|
+
[Concepts](docs/concepts.md) gives more detail about SemCom and about how `nexgenpy` fits a SemCom pipeline.
|
|
223
|
+
|
|
224
|
+
## Documentation
|
|
225
|
+
|
|
226
|
+
| Document | Contents |
|
|
227
|
+
|---|---|
|
|
228
|
+
| [Quick start](docs/quickstart.md) | A full example: register, load and run a coding and a decoding actuator |
|
|
229
|
+
| [Concepts](docs/concepts.md) | SemCom background, the two objectives, and how actuators, data and contents fit a SemCom pipeline |
|
|
230
|
+
| [Architecture](docs/architecture.md) | Modules, the request flow for each operation and the local directory layout |
|
|
231
|
+
| [Writing actuators](docs/writing-actuators.md) | Rules and examples for actuator functions |
|
|
232
|
+
| [Configuration](docs/configuration.md) | Environment variables, API key, timeouts and logging |
|
|
233
|
+
| [API: `actuator`](docs/api/actuator.md) | The decorator that registers an actuator |
|
|
234
|
+
| [API: `load_actuator`](docs/api/load-actuator.md) | The function that downloads an actuator and installs its requirements |
|
|
235
|
+
| [API: `run_actuator` and `Actuator`](docs/api/run-actuator.md) | The function and the class that run an actuator and send its output |
|
|
236
|
+
| [Development](docs/development.md) | Tests, lint, CI, release and code conventions |
|
|
237
|
+
| [Security](docs/security.md) | Risks of downloaded actuator code, and SemCom threats |
|
|
238
|
+
| [Glossary](docs/glossary.md) | SemCom and project terms |
|
|
239
|
+
| [References](docs/references.md) | Surveys and papers about SemCom |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
nexgenpy/__init__.py,sha256=cQLjAlSrgGI444WYxI_cFkeoz6dvaSXTjpqfafqkdNw,91
|
|
2
|
+
nexgenpy/actuators.py,sha256=vDaozWVKMXtoEmaonWISWpWYb1RbbWd47Y7UweQdl_g,5784
|
|
3
|
+
nexgenpy/manager.py,sha256=WvoISC87leaPjw2-298qMGt5zDU2qArVEIndmcxPTB4,21661
|
|
4
|
+
nexgenpy-1.0.0.dist-info/METADATA,sha256=HbaSjRclfa5wPc3SagKozf2gSdHGacq3BMhN_7P_-kE,10686
|
|
5
|
+
nexgenpy-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
nexgenpy-1.0.0.dist-info/top_level.txt,sha256=R5Pm2g3M-7W0ZI2kxttJUkV4mAm8oMUu0EacvaEpvkA,9
|
|
7
|
+
nexgenpy-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nexgenpy
|