flwr-nightly 1.23.0.dev20251030__py3-none-any.whl → 1.23.0.dev20251031__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

flwr/cli/new/new.py CHANGED
@@ -15,14 +15,20 @@
15
15
  """Flower command line interface `new` command."""
16
16
 
17
17
 
18
+ import io
19
+ import json
18
20
  import re
21
+ import zipfile
19
22
  from enum import Enum
20
23
  from pathlib import Path
21
24
  from string import Template
22
25
  from typing import Annotated, Optional
23
26
 
27
+ import requests
24
28
  import typer
25
29
 
30
+ from flwr.supercore.constant import APP_ID_PATTERN, PLATFORM_API_URL
31
+
26
32
  from ..utils import (
27
33
  is_valid_project_name,
28
34
  prompt_options,
@@ -93,6 +99,180 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
93
99
  create_file(file_path, content)
94
100
 
95
101
 
102
+ def print_success_prompt(
103
+ package_name: str, llm_challenge_str: Optional[str] = None
104
+ ) -> None:
105
+ """Print styled setup instructions for running a new Flower App after creation."""
106
+ prompt = typer.style(
107
+ "🎊 Flower App creation successful.\n\n"
108
+ "To run your Flower App, first install its dependencies:\n\n",
109
+ fg=typer.colors.GREEN,
110
+ bold=True,
111
+ )
112
+
113
+ _add = " huggingface-cli login\n" if llm_challenge_str else ""
114
+
115
+ prompt += typer.style(
116
+ f" cd {package_name} && pip install -e .\n" + _add + "\n",
117
+ fg=typer.colors.BRIGHT_CYAN,
118
+ bold=True,
119
+ )
120
+
121
+ prompt += typer.style(
122
+ "then, run the app:\n\n ",
123
+ fg=typer.colors.GREEN,
124
+ bold=True,
125
+ )
126
+
127
+ prompt += typer.style(
128
+ "\tflwr run .\n\n",
129
+ fg=typer.colors.BRIGHT_CYAN,
130
+ bold=True,
131
+ )
132
+
133
+ prompt += typer.style(
134
+ "💡 Check the README in your app directory to learn how to\n"
135
+ "customize it and how to run it using the Deployment Runtime.\n",
136
+ fg=typer.colors.GREEN,
137
+ bold=True,
138
+ )
139
+
140
+ print(prompt)
141
+
142
+
143
+ # Security: prevent zip-slip
144
+ def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
145
+ """Extract ZIP file into destination directory."""
146
+ dest_dir = dest_dir.resolve()
147
+
148
+ def _is_within_directory(base: Path, target: Path) -> bool:
149
+ try:
150
+ target.relative_to(base)
151
+ return True
152
+ except ValueError:
153
+ return False
154
+
155
+ for member in zf.infolist():
156
+ # Skip directory placeholders;
157
+ # ZipInfo can represent them as names ending with '/'.
158
+ if member.is_dir():
159
+ target_path = (dest_dir / member.filename).resolve()
160
+ if not _is_within_directory(dest_dir, target_path):
161
+ raise ValueError(f"Unsafe path in zip: {member.filename}")
162
+ target_path.mkdir(parents=True, exist_ok=True)
163
+ continue
164
+
165
+ # Files
166
+ target_path = (dest_dir / member.filename).resolve()
167
+ if not _is_within_directory(dest_dir, target_path):
168
+ raise ValueError(f"Unsafe path in zip: {member.filename}")
169
+
170
+ # Ensure parent exists
171
+ target_path.parent.mkdir(parents=True, exist_ok=True)
172
+
173
+ # Extract
174
+ with zf.open(member, "r") as src, open(target_path, "wb") as dst:
175
+ dst.write(src.read())
176
+
177
+
178
+ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
179
+ """Download ZIP file from Platform API to memory."""
180
+ try:
181
+ r = requests.get(presigned_url, timeout=60)
182
+ r.raise_for_status()
183
+ except requests.RequestException as e:
184
+ raise typer.BadParameter(f"ZIP download failed: {e}") from e
185
+
186
+ buf = io.BytesIO(r.content)
187
+ # Validate it's a zip
188
+ if not zipfile.is_zipfile(buf):
189
+ raise typer.BadParameter("Downloaded file is not a valid ZIP")
190
+ buf.seek(0)
191
+ return buf
192
+
193
+
194
+ def _request_download_link(identifier: str) -> str:
195
+ """Request download link from Flower platform API."""
196
+ url = f"{PLATFORM_API_URL}/hub/fetch-zip"
197
+ headers = {
198
+ "Content-Type": "application/json",
199
+ "Accept": "application/json",
200
+ }
201
+ body = {
202
+ "identifier": identifier, # send raw string of identifier
203
+ }
204
+
205
+ try:
206
+ resp = requests.post(url, headers=headers, data=json.dumps(body), timeout=20)
207
+ except requests.RequestException as e:
208
+ raise typer.BadParameter(f"Unable to connect to Platform API: {e}") from e
209
+
210
+ if resp.status_code == 404:
211
+ raise typer.BadParameter(f"'{identifier}' not found in Platform API")
212
+ if not resp.ok:
213
+ raise typer.BadParameter(
214
+ f"Platform API request failed with "
215
+ f"status {resp.status_code}. Details: {resp.text}"
216
+ )
217
+
218
+ data = resp.json()
219
+ if "zip_url" not in data:
220
+ raise typer.BadParameter("Invalid response from Platform API")
221
+ return str(data["zip_url"])
222
+
223
+
224
+ def download_remote_app_via_api(identifier: str) -> None:
225
+ """Download App from Platform API."""
226
+ # Parse @user/app just to derive local dir name
227
+ m = re.match(APP_ID_PATTERN, identifier)
228
+ if not m:
229
+ raise typer.BadParameter(
230
+ "Invalid remote app ID. Expected format: '@user_name/app_name'."
231
+ )
232
+ app_name = m.group("app")
233
+
234
+ project_dir = Path.cwd() / app_name
235
+ if project_dir.exists():
236
+ if not typer.confirm(
237
+ typer.style(
238
+ f"\n💬 {app_name} already exists, do you want to override it?",
239
+ fg=typer.colors.MAGENTA,
240
+ bold=True,
241
+ )
242
+ ):
243
+ return
244
+
245
+ print(
246
+ typer.style(
247
+ f"\n🔗 Requesting download link for {identifier}...",
248
+ fg=typer.colors.GREEN,
249
+ bold=True,
250
+ )
251
+ )
252
+ presigned_url = _request_download_link(identifier)
253
+
254
+ print(
255
+ typer.style(
256
+ "⬇️ Downloading ZIP into memory...",
257
+ fg=typer.colors.GREEN,
258
+ bold=True,
259
+ )
260
+ )
261
+ zip_buf = _download_zip_to_memory(presigned_url)
262
+
263
+ print(
264
+ typer.style(
265
+ f"📦 Unpacking into {project_dir}...",
266
+ fg=typer.colors.GREEN,
267
+ bold=True,
268
+ )
269
+ )
270
+ with zipfile.ZipFile(zip_buf) as zf:
271
+ _safe_extract_zip(zf, Path.cwd())
272
+
273
+ print_success_prompt(app_name)
274
+
275
+
96
276
  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
97
277
  def new(
98
278
  app_name: Annotated[
@@ -111,6 +291,12 @@ def new(
111
291
  """Create new Flower App."""
112
292
  if app_name is None:
113
293
  app_name = prompt_text("Please provide the app name")
294
+
295
+ # Download remote app
296
+ if app_name and app_name.startswith("@"):
297
+ download_remote_app_via_api(app_name)
298
+ return
299
+
114
300
  if not is_valid_project_name(app_name):
115
301
  app_name = prompt_text(
116
302
  "Please provide a name that only contains "
@@ -282,38 +468,4 @@ def new(
282
468
  context=context,
283
469
  )
284
470
 
285
- prompt = typer.style(
286
- "🎊 Flower App creation successful.\n\n"
287
- "To run your Flower App, first install its dependencies:\n\n",
288
- fg=typer.colors.GREEN,
289
- bold=True,
290
- )
291
-
292
- _add = " huggingface-cli login\n" if llm_challenge_str else ""
293
-
294
- prompt += typer.style(
295
- f" cd {package_name} && pip install -e .\n" + _add + "\n",
296
- fg=typer.colors.BRIGHT_CYAN,
297
- bold=True,
298
- )
299
-
300
- prompt += typer.style(
301
- "then, run the app:\n\n ",
302
- fg=typer.colors.GREEN,
303
- bold=True,
304
- )
305
-
306
- prompt += typer.style(
307
- "\tflwr run .\n\n",
308
- fg=typer.colors.BRIGHT_CYAN,
309
- bold=True,
310
- )
311
-
312
- prompt += typer.style(
313
- "💡 Check the README in your app directory to learn how to\n"
314
- "customize it and how to run it using the Deployment Runtime.\n",
315
- fg=typer.colors.GREEN,
316
- bold=True,
317
- )
318
-
319
- print(prompt)
471
+ print_success_prompt(package_name, llm_challenge_str)
@@ -44,10 +44,9 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
44
44
  ] = None,
45
45
  ) -> Iterator[
46
46
  tuple[
47
+ int,
47
48
  Callable[[], Optional[tuple[Message, ObjectTree]]],
48
49
  Callable[[Message, ObjectTree], set[str]],
49
- Callable[[], Optional[int]],
50
- Callable[[], None],
51
50
  Callable[[int], Run],
52
51
  Callable[[str, int], Fab],
53
52
  Callable[[int, str], bytes],
@@ -81,12 +80,11 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
81
80
 
82
81
  Returns
83
82
  -------
83
+ node_id : int
84
84
  receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
85
85
  send : Callable[[Message, ObjectTree], set[str]]
86
- create_node : Optional[Callable]
87
- delete_node : Optional[Callable]
88
- get_run : Optional[Callable]
89
- get_fab : Optional[Callable]
86
+ get_run : Callable[[int], Run]
87
+ get_fab : Callable[[str, int], Fab]
90
88
  pull_object : Callable[[str], bytes]
91
89
  push_object : Callable[[str, bytes], None]
92
90
  confirm_message_received : Callable[[str], None]
@@ -19,7 +19,7 @@ from collections.abc import Iterator, Sequence
19
19
  from contextlib import contextmanager
20
20
  from logging import ERROR
21
21
  from pathlib import Path
22
- from typing import Callable, Optional, Union, cast
22
+ from typing import Callable, Optional, Union
23
23
 
24
24
  import grpc
25
25
  from cryptography.hazmat.primitives.asymmetric import ec
@@ -45,12 +45,15 @@ from flwr.common.serde import (
45
45
  from flwr.common.typing import Fab, Run
46
46
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
47
47
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
48
- CreateNodeRequest,
49
- DeleteNodeRequest,
48
+ ActivateNodeRequest,
49
+ ActivateNodeResponse,
50
+ DeactivateNodeRequest,
50
51
  PullMessagesRequest,
51
52
  PullMessagesResponse,
52
53
  PushMessagesRequest,
53
54
  PushMessagesResponse,
55
+ RegisterNodeFleetRequest,
56
+ UnregisterNodeFleetRequest,
54
57
  )
55
58
  from flwr.proto.fleet_pb2_grpc import FleetStub # pylint: disable=E0611
56
59
  from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
@@ -79,10 +82,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
79
82
  adapter_cls: Optional[Union[type[FleetStub], type[GrpcAdapter]]] = None,
80
83
  ) -> Iterator[
81
84
  tuple[
85
+ int,
82
86
  Callable[[], Optional[tuple[Message, ObjectTree]]],
83
87
  Callable[[Message, ObjectTree], set[str]],
84
- Callable[[], Optional[int]],
85
- Callable[[], None],
86
88
  Callable[[int], Run],
87
89
  Callable[[str, int], Fab],
88
90
  Callable[[int, str], bytes],
@@ -125,11 +127,11 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
125
127
 
126
128
  Returns
127
129
  -------
128
- receive : Callable
129
- send : Callable
130
- create_node : Optional[Callable]
131
- delete_node : Optional[Callable]
132
- get_run : Optional[Callable]
130
+ node_id : int
131
+ receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
132
+ send : Callable[[Message, ObjectTree], set[str]]
133
+ get_run : Callable[[int], Run]
134
+ get_fab : Callable[[str, int], Fab]
133
135
  pull_object : Callable[[str], bytes]
134
136
  push_object : Callable[[str, bytes], None]
135
137
  confirm_message_received : Callable[[str], None]
@@ -138,7 +140,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
138
140
  root_certificates = Path(root_certificates).read_bytes()
139
141
 
140
142
  # Automatic node auth: generate keys if user didn't provide any
143
+ self_registered = False
141
144
  if authentication_keys is None:
145
+ self_registered = True
142
146
  authentication_keys = generate_key_pairs()
143
147
 
144
148
  # Always configure auth interceptor, with either user-provided or generated keys
@@ -164,7 +168,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
164
168
  # Wrap stub
165
169
  _wrap_stub(stub, retry_invoker)
166
170
  ###########################################################################
167
- # send_node_heartbeat/create_node/delete_node/receive/send/get_run functions
171
+ # SuperNode functions
168
172
  ###########################################################################
169
173
 
170
174
  def send_node_heartbeat() -> bool:
@@ -201,23 +205,26 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
201
205
 
202
206
  heartbeat_sender = HeartbeatSender(send_node_heartbeat)
203
207
 
204
- def create_node() -> Optional[int]:
205
- """Set create_node."""
206
- # Call FleetAPI
207
- create_node_request = CreateNodeRequest(
208
+ def register_node() -> None:
209
+ """Register node with SuperLink."""
210
+ stub.RegisterNode(RegisterNodeFleetRequest(public_key=node_pk))
211
+
212
+ def activate_node() -> int:
213
+ """Activate node and start heartbeat."""
214
+ req = ActivateNodeRequest(
208
215
  public_key=node_pk,
209
216
  heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
210
217
  )
211
- create_node_response = stub.CreateNode(request=create_node_request)
218
+ res: ActivateNodeResponse = stub.ActivateNode(req)
212
219
 
213
220
  # Remember the node and start the heartbeat sender
214
221
  nonlocal node
215
- node = cast(Node, create_node_response.node)
222
+ node = Node(node_id=res.node_id)
216
223
  heartbeat_sender.start()
217
224
  return node.node_id
218
225
 
219
- def delete_node() -> None:
220
- """Set delete_node."""
226
+ def deactivate_node() -> None:
227
+ """Deactivate node and stop heartbeat."""
221
228
  # Get Node
222
229
  nonlocal node
223
230
  if node is None:
@@ -228,8 +235,20 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
228
235
  heartbeat_sender.stop()
229
236
 
230
237
  # Call FleetAPI
231
- delete_node_request = DeleteNodeRequest(node=node)
232
- stub.DeleteNode(request=delete_node_request)
238
+ req = DeactivateNodeRequest(node_id=node.node_id)
239
+ stub.DeactivateNode(req)
240
+
241
+ def unregister_node() -> None:
242
+ """Unregister node from SuperLink."""
243
+ # Get Node
244
+ nonlocal node
245
+ if node is None:
246
+ log(ERROR, "Node instance missing")
247
+ return
248
+
249
+ # Call FleetAPI
250
+ req = UnregisterNodeFleetRequest(node_id=node.node_id)
251
+ stub.UnregisterNode(req)
233
252
 
234
253
  # Cleanup
235
254
  node = None
@@ -336,12 +355,14 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
336
355
  fn(object_id)
337
356
 
338
357
  try:
358
+ if self_registered:
359
+ register_node()
360
+ node_id = activate_node()
339
361
  # Yield methods
340
362
  yield (
363
+ node_id,
341
364
  receive,
342
365
  send,
343
- create_node,
344
- delete_node,
345
366
  get_run,
346
367
  get_fab,
347
368
  pull_object,
@@ -356,7 +377,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
356
377
  if node is not None:
357
378
  # Disable retrying
358
379
  retry_invoker.max_tries = 1
359
- delete_node()
380
+ deactivate_node()
381
+ if self_registered:
382
+ unregister_node()
360
383
  except grpc.RpcError:
361
384
  pass
362
385
  channel.close()
@@ -36,12 +36,8 @@ from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=
36
36
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
37
37
  ActivateNodeRequest,
38
38
  ActivateNodeResponse,
39
- CreateNodeRequest,
40
- CreateNodeResponse,
41
39
  DeactivateNodeRequest,
42
40
  DeactivateNodeResponse,
43
- DeleteNodeRequest,
44
- DeleteNodeResponse,
45
41
  PullMessagesRequest,
46
42
  PullMessagesResponse,
47
43
  PushMessagesRequest,
@@ -126,18 +122,6 @@ class GrpcAdapter:
126
122
  response.ParseFromString(container_res.grpc_message_content)
127
123
  return response
128
124
 
129
- def CreateNode( # pylint: disable=C0103
130
- self, request: CreateNodeRequest, **kwargs: Any
131
- ) -> CreateNodeResponse:
132
- """."""
133
- return self._send_and_receive(request, CreateNodeResponse, **kwargs)
134
-
135
- def DeleteNode( # pylint: disable=C0103
136
- self, request: DeleteNodeRequest, **kwargs: Any
137
- ) -> DeleteNodeResponse:
138
- """."""
139
- return self._send_and_receive(request, DeleteNodeResponse, **kwargs)
140
-
141
125
  def RegisterNode( # pylint: disable=C0103
142
126
  self, request: RegisterNodeFleetRequest, **kwargs: Any
143
127
  ) -> RegisterNodeFleetResponse:
@@ -15,7 +15,6 @@
15
15
  """Contextmanager for a REST request-response channel to the Flower server."""
16
16
 
17
17
 
18
- import secrets
19
18
  from collections.abc import Iterator
20
19
  from contextlib import contextmanager
21
20
  from logging import ERROR, WARN
@@ -46,14 +45,18 @@ from flwr.common.serde import (
46
45
  from flwr.common.typing import Fab, Run
47
46
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
48
47
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
49
- CreateNodeRequest,
50
- CreateNodeResponse,
51
- DeleteNodeRequest,
52
- DeleteNodeResponse,
48
+ ActivateNodeRequest,
49
+ ActivateNodeResponse,
50
+ DeactivateNodeRequest,
51
+ DeactivateNodeResponse,
53
52
  PullMessagesRequest,
54
53
  PullMessagesResponse,
55
54
  PushMessagesRequest,
56
55
  PushMessagesResponse,
56
+ RegisterNodeFleetRequest,
57
+ RegisterNodeFleetResponse,
58
+ UnregisterNodeFleetRequest,
59
+ UnregisterNodeFleetResponse,
57
60
  )
58
61
  from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
59
62
  SendNodeHeartbeatRequest,
@@ -70,6 +73,7 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
70
73
  )
71
74
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
72
75
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
76
+ from flwr.supercore.primitives.asymmetric import generate_key_pairs, public_key_to_bytes
73
77
 
74
78
  try:
75
79
  import requests
@@ -77,8 +81,10 @@ except ModuleNotFoundError:
77
81
  flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
78
82
 
79
83
 
80
- PATH_CREATE_NODE: str = "api/v0/fleet/create-node"
81
- PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
84
+ PATH_REGISTER_NODE: str = "/api/v0/fleet/register-node"
85
+ PATH_ACTIVATE_NODE: str = "/api/v0/fleet/activate-node"
86
+ PATH_DEACTIVATE_NODE: str = "/api/v0/fleet/deactivate-node"
87
+ PATH_UNREGISTER_NODE: str = "/api/v0/fleet/unregister-node"
82
88
  PATH_PULL_MESSAGES: str = "/api/v0/fleet/pull-messages"
83
89
  PATH_PUSH_MESSAGES: str = "/api/v0/fleet/push-messages"
84
90
  PATH_PULL_OBJECT: str = "/api/v0/fleet/pull-object"
@@ -105,10 +111,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
105
111
  ] = None,
106
112
  ) -> Iterator[
107
113
  tuple[
114
+ int,
108
115
  Callable[[], Optional[tuple[Message, ObjectTree]]],
109
116
  Callable[[Message, ObjectTree], set[str]],
110
- Callable[[], Optional[int]],
111
- Callable[[], None],
112
117
  Callable[[int], Run],
113
118
  Callable[[str, int], Fab],
114
119
  Callable[[int, str], bytes],
@@ -144,11 +149,11 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
144
149
 
145
150
  Returns
146
151
  -------
147
- receive : Callable
148
- send : Callable
149
- create_node : Optional[Callable]
150
- delete_node : Optional[Callable]
151
- get_run : Optional[Callable]
152
+ node_id : int
153
+ receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
154
+ send : Callable[[Message, ObjectTree], set[str]]
155
+ get_run : Callable[[int], Run]
156
+ get_fab : Callable[[str, int], Fab]
152
157
  pull_object : Callable[[str], bytes]
153
158
  push_object : Callable[[str, bytes], None]
154
159
  confirm_message_received : Callable[[str], None]
@@ -179,6 +184,13 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
179
184
  if authentication_keys is not None:
180
185
  log(ERROR, "Client authentication is not supported for this transport type.")
181
186
 
187
+ # REST does NOT support node authentication
188
+ self_registered = False
189
+ if authentication_keys is None:
190
+ self_registered = True
191
+ authentication_keys = generate_key_pairs()
192
+ node_pk = public_key_to_bytes(authentication_keys[1])
193
+
182
194
  # Shared variables for inner functions
183
195
  node: Optional[Node] = None
184
196
 
@@ -186,7 +198,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
186
198
  retry_invoker.should_giveup = None
187
199
 
188
200
  ###########################################################################
189
- # heartbeat/create_node/delete_node/receive/send/get_run functions
201
+ # SuperNode functions
190
202
  ###########################################################################
191
203
 
192
204
  def _request(
@@ -296,28 +308,35 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
296
308
 
297
309
  heartbeat_sender = HeartbeatSender(send_node_heartbeat)
298
310
 
299
- def create_node() -> Optional[int]:
300
- """Set create_node."""
301
- req = CreateNodeRequest(
302
- # REST does not support node authentication;
303
- # random bytes are used instead
304
- public_key=secrets.token_bytes(32),
311
+ def register_node() -> None:
312
+ """Register node with SuperLink."""
313
+ req = RegisterNodeFleetRequest(public_key=node_pk)
314
+
315
+ # Send the request
316
+ res = _request(req, RegisterNodeFleetResponse, PATH_REGISTER_NODE)
317
+ if res is None:
318
+ raise RuntimeError("Failed to register node")
319
+
320
+ def activate_node() -> int:
321
+ """Activate node and start heartbeat."""
322
+ req = ActivateNodeRequest(
323
+ public_key=node_pk,
305
324
  heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
306
325
  )
307
326
 
308
327
  # Send the request
309
- res = _request(req, CreateNodeResponse, PATH_CREATE_NODE)
328
+ res = _request(req, ActivateNodeResponse, PATH_ACTIVATE_NODE)
310
329
  if res is None:
311
- return None
330
+ raise RuntimeError("Failed to activate node")
312
331
 
313
332
  # Remember the node and start the heartbeat sender
314
333
  nonlocal node
315
- node = res.node
334
+ node = Node(node_id=res.node_id)
316
335
  heartbeat_sender.start()
317
336
  return node.node_id
318
337
 
319
- def delete_node() -> None:
320
- """Set delete_node."""
338
+ def deactivate_node() -> None:
339
+ """Deactivate node and stop heartbeat."""
321
340
  nonlocal node
322
341
  if node is None:
323
342
  raise RuntimeError("Node instance missing")
@@ -325,13 +344,27 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
325
344
  # Stop the heartbeat sender
326
345
  heartbeat_sender.stop()
327
346
 
328
- # Send DeleteNode request
329
- req = DeleteNodeRequest(node=node)
347
+ # Send DeactivateNode request
348
+ req = DeactivateNodeRequest(node_id=node.node_id)
349
+
350
+ # Send the request
351
+ res = _request(req, DeactivateNodeResponse, PATH_DEACTIVATE_NODE)
352
+ if res is None:
353
+ raise RuntimeError("Failed to deactivate node")
354
+
355
+ def unregister_node() -> None:
356
+ """Unregister node from SuperLink."""
357
+ nonlocal node
358
+ if node is None:
359
+ raise RuntimeError("Node instance missing")
360
+
361
+ # Send UnregisterNode request
362
+ req = UnregisterNodeFleetRequest(node_id=node.node_id)
330
363
 
331
364
  # Send the request
332
- res = _request(req, DeleteNodeResponse, PATH_DELETE_NODE)
365
+ res = _request(req, UnregisterNodeFleetResponse, PATH_UNREGISTER_NODE)
333
366
  if res is None:
334
- return
367
+ raise RuntimeError("Failed to unregister node")
335
368
 
336
369
  # Cleanup
337
370
  node = None
@@ -447,12 +480,14 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
447
480
  fn(object_id)
448
481
 
449
482
  try:
483
+ if self_registered:
484
+ register_node()
485
+ node_id = activate_node()
450
486
  # Yield methods
451
487
  yield (
488
+ node_id,
452
489
  receive,
453
490
  send,
454
- create_node,
455
- delete_node,
456
491
  get_run,
457
492
  get_fab,
458
493
  pull_object,
@@ -467,6 +502,8 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
467
502
  if node is not None:
468
503
  # Disable retrying
469
504
  retry_invoker.max_tries = 1
470
- delete_node()
505
+ deactivate_node()
506
+ if self_registered:
507
+ unregister_node()
471
508
  except RequestsConnectionError:
472
509
  pass