remoteRF-server 1.1.0.dev28__py3-none-any.whl → 1.1.0.dev30__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.
@@ -148,7 +148,7 @@ def _table() -> list[Command]:
148
148
  Command(("global verify",), "Redeem the emailed code [code]", _global("verify"), "Global"),
149
149
  Command(("global leave",), "Stop participating", _global("leave"), "Global"),
150
150
  # ---- clusters (sharing devices with other labs) ----
151
- Command(("cluster list", "cluster"), "Clusters shared with other labs", _federation("list"), "Clusters"),
151
+ Command(("cluster list",), "Clusters shared with other labs", _federation("list"), "Clusters"),
152
152
  Command(("cluster create",), "Start a cluster: cluster create <name>", _federation("create"), "Clusters"),
153
153
  Command(("cluster invite",), "Invite code for a lab: cluster invite <name>", _federation("invite"), "Clusters"),
154
154
  Command(("cluster join",), "Join with a code: cluster join <code>", _federation("join"), "Clusters"),
@@ -32,6 +32,17 @@ def _print(text: str, style=None) -> None:
32
32
  printf(text, style or Sty.DEFAULT)
33
33
 
34
34
 
35
+ def _lab(party: dict) -> str:
36
+ short = party["deployment_id"][:8]
37
+ return f"{party['display_name']} ({short}…)" if party.get("display_name") else party["deployment_id"]
38
+
39
+
40
+ def _day(epoch) -> str:
41
+ import datetime
42
+
43
+ return datetime.datetime.fromtimestamp(int(epoch)).strftime("%Y-%m-%d")
44
+
45
+
35
46
  def cmd_clusters() -> int:
36
47
  from ...common.utils.panel import marker, print_panel
37
48
  from . import cluster
@@ -48,10 +59,22 @@ def cmd_clusters() -> int:
48
59
  items = cluster.list_clusters()
49
60
  if not items:
50
61
  rows.append("No clusters. Start one with `cluster create <name>`.")
51
- width = max([len(item["name"]) for item in items] or [0])
52
- for item in items:
53
- peer = item.get("peer") or "-"
54
- rows.append(f"{item['name']:<{width}} {item['role']:<6} {item['state']:<9} peer={peer}")
62
+ for index, item in enumerate(items):
63
+ if index:
64
+ rows.append("")
65
+ if item["role"] == "owner":
66
+ count = len(item["members"])
67
+ rows.append(f"{item['name']} (yours) {count} member lab{'s' if count != 1 else ''}"
68
+ + (f", {item['open_invites']} open invite{'s' if item['open_invites'] != 1 else ''}"
69
+ if item["open_invites"] else ""))
70
+ for member in item["members"]:
71
+ rows.append(f" {_lab(member)} {member['state'] or 'joining'}"
72
+ + (f" since {_day(member['joined_at'])}" if member.get("joined_at") else ""))
73
+ if not item["members"] and not item["open_invites"]:
74
+ rows.append(f" nobody yet: `cluster invite {item['name']}`")
75
+ else:
76
+ owner = _lab({"display_name": item.get("peer_name"), "deployment_id": item["peer"]})
77
+ rows.append(f"{item['name']} owned by {owner} {item['state']}")
55
78
  print()
56
79
  print_panel("Clusters", rows, literal=True)
57
80
  print()
@@ -66,9 +66,39 @@ def _state_path() -> Path:
66
66
 
67
67
  def load() -> dict:
68
68
  try:
69
- return json.loads(_state_path().read_text())
69
+ clusters = json.loads(_state_path().read_text())
70
70
  except FileNotFoundError:
71
71
  return {}
72
+ for cluster in clusters.values():
73
+ _upgrade(cluster)
74
+ return clusters
75
+
76
+
77
+ def _upgrade(cluster: dict) -> None:
78
+ """First shape held one partner per owner cluster; now it holds a star."""
79
+ if cluster.get("role") != "owner" or "members" in cluster:
80
+ return
81
+ members, invites = {}, {}
82
+ if cluster.get("peer"):
83
+ members[cluster["peer"]] = {
84
+ "display_name": None, "contract_id": cluster.get("contract_id"),
85
+ "state": cluster.get("state"), "published": cluster.get("published", []),
86
+ "joined_at": cluster.get("created_at"),
87
+ }
88
+ if cluster.get("joiner"):
89
+ members[cluster["peer"]]["joiner"] = cluster["joiner"]
90
+ elif cluster.get("joiner"):
91
+ joiner = cluster["joiner"]
92
+ members[joiner["deployment_id"]] = {
93
+ "display_name": None, "contract_id": None, "state": "pending",
94
+ "published": [], "joined_at": cluster.get("created_at"), "joiner": joiner,
95
+ }
96
+ elif cluster.get("secret") and cluster.get("invite_id"):
97
+ invites[cluster["invite_id"]] = {"secret": cluster["secret"], "auto": cluster.get("auto", True),
98
+ "created_at": cluster.get("created_at")}
99
+ for key in ("peer", "contract_id", "state", "published", "joiner", "secret", "invite_id", "auto"):
100
+ cluster.pop(key, None)
101
+ cluster["members"], cluster["invites"] = members, invites
72
102
 
73
103
 
74
104
  def save(clusters: dict) -> None:
@@ -186,18 +216,19 @@ def create(name: str) -> dict:
186
216
  clusters = load()
187
217
  if name in clusters:
188
218
  raise ClusterError(f"cluster {name} already exists")
189
- clusters[name] = {"role": "owner", "state": "created", "created_at": int(time.time())}
219
+ clusters[name] = {"role": "owner", "created_at": int(time.time()), "members": {}, "invites": {}}
190
220
  save(clusters)
191
221
  return clusters[name]
192
222
 
193
223
 
194
224
  def invite(name: str, *, auto: bool = True) -> str:
195
- """The code is the approval: whoever redeems it is pinned and contracted
196
- without a further step here (`auto=False` keeps a manual `accept`)."""
225
+ """One code admits one lab. The code is the approval: whoever redeems it is
226
+ pinned and contracted without a further step here (`auto=False` keeps a
227
+ manual `accept`). Several codes may be open at once."""
197
228
  clusters = load()
198
229
  cluster = _find(clusters, name)
199
230
  if cluster["role"] != "owner":
200
- raise ClusterError(f"{name} is owned by another deployment")
231
+ raise ClusterError(f"{name} is owned by another deployment; only the owner invites")
201
232
  me = _pin_self()
202
233
  invite_id, secret = secrets.token_hex(5), secrets.token_bytes(20)
203
234
  g.call("POST", "/v2/broker/invites", {
@@ -208,7 +239,7 @@ def invite(name: str, *, auto: bool = True) -> str:
208
239
  "identity": me,
209
240
  "tag": _tag(secret, invite_id, me),
210
241
  })
211
- cluster.update(state="invited", invite_id=invite_id, secret=secret.hex(), auto=auto)
242
+ cluster["invites"][invite_id] = {"secret": secret.hex(), "auto": auto, "created_at": int(time.time())}
212
243
  save(clusters)
213
244
  return encode_code(invite_id, secret)
214
245
 
@@ -230,10 +261,13 @@ def join(code: str) -> str:
230
261
  _pin(owner["identity"], source="cluster_invite")
231
262
  name = view["cluster_name"]
232
263
  clusters = load()
264
+ if name in clusters and clusters[name]["role"] == "owner":
265
+ raise ClusterError(f"{name} is your own cluster")
233
266
  clusters[name] = {
234
267
  "role": "member",
235
268
  "state": "joined",
236
269
  "peer": owner["identity"]["deployment_id"],
270
+ "peer_name": owner.get("display_name"),
237
271
  "invite_id": invite_id,
238
272
  "created_at": int(time.time()),
239
273
  }
@@ -241,13 +275,9 @@ def join(code: str) -> str:
241
275
  return name
242
276
 
243
277
 
244
- def accept(name: str) -> dict:
245
- """Owner: pin the joiner and propose the contract."""
246
- clusters = load()
247
- cluster = _find(clusters, name)
248
- if cluster.get("state") != "pending":
249
- raise ClusterError(f"{name} has no join waiting for approval")
250
- joiner = cluster["joiner"]
278
+ def _propose(cluster: dict, member: dict, peer: str) -> None:
279
+ """Owner: pin the joiner and propose its bilateral contract."""
280
+ joiner = member.pop("joiner")
251
281
  _pin(joiner, source="cluster_invite")
252
282
  me = _identity()
253
283
  a, b = sorted([me, joiner], key=lambda item: item["deployment_id"])
@@ -267,29 +297,49 @@ def accept(name: str) -> dict:
267
297
  "supersedes_version": None,
268
298
  }
269
299
  _contracts().propose(terms, actor_admin_ref="cluster")
270
- cluster.update(state="proposed", peer=joiner["deployment_id"], contract_id=terms["contract_id"])
271
- cluster.pop("joiner", None)
272
- cluster.pop("secret", None)
300
+ member.update(state="proposed", contract_id=terms["contract_id"])
301
+ _publish(member)
302
+
303
+
304
+ def accept(name: str, peer: str | None = None) -> dict:
305
+ """Owner, manual invites only: approve a join that is waiting."""
306
+ clusters = load()
307
+ cluster = _find(clusters, name)
308
+ pending = {pid: m for pid, m in cluster.get("members", {}).items() if m.get("state") == "pending"}
309
+ if not pending:
310
+ raise ClusterError(f"{name} has no join waiting for approval")
311
+ if peer is None and len(pending) > 1:
312
+ raise ClusterError(f"{name} has several joins waiting; name one: " + ", ".join(sorted(pending)))
313
+ peer = peer or next(iter(pending))
314
+ if peer not in pending:
315
+ raise ClusterError(f"{peer} is not waiting to join {name}")
316
+ _propose(cluster, pending[peer], peer)
273
317
  save(clusters)
274
- _publish(cluster)
275
318
  return cluster
276
319
 
277
320
 
321
+ def _revoke(contract_id: str | None) -> None:
322
+ if not contract_id:
323
+ return
324
+ try:
325
+ _contracts().revoke(contract_id, 1, actor_admin_ref="cluster")
326
+ except ContractError as exc:
327
+ if exc.code not in ("contract_revoked", "contract_terminal", "invalid_transition"):
328
+ raise
329
+
330
+
278
331
  def remove(name: str) -> str:
279
- """Leave or disband: revoke the contract (terminal, signed, published so
280
- the peer's next sync sees it) and forget the cluster locally."""
332
+ """Leave (member) or disband (owner): revoke every contract -- a terminal,
333
+ signed event the other side imports on its next sync -- and forget the
334
+ cluster locally."""
281
335
  clusters = load()
282
336
  cluster = _find(clusters, name)
283
337
  outcome = "removed"
284
- if cluster.get("contract_id"):
285
- store = _contracts()
338
+ parties = list(cluster["members"].values()) if cluster["role"] == "owner" else [cluster]
339
+ for party in parties:
340
+ _revoke(party.get("contract_id"))
286
341
  try:
287
- store.revoke(cluster["contract_id"], 1, actor_admin_ref="cluster")
288
- except ContractError as exc:
289
- if exc.code not in ("contract_revoked", "contract_terminal", "invalid_transition"):
290
- raise
291
- try:
292
- _publish(cluster) # the other lab imports the revocation on its next sync
342
+ _publish(party)
293
343
  except g.GlobalError:
294
344
  outcome = "removed locally; could not reach Global to tell the other lab"
295
345
  del clusters[name]
@@ -298,30 +348,48 @@ def remove(name: str) -> str:
298
348
 
299
349
 
300
350
  def list_clusters() -> list[dict]:
351
+ """Owner rows carry `members` and `open_invites`; member rows carry `peer`."""
301
352
  store = None
302
353
  out = []
303
354
  for name, cluster in sorted(load().items()):
304
- item = {"name": name, **cluster}
305
- item.pop("secret", None)
306
- if cluster.get("contract_id"):
307
- try:
308
- store = store or _contracts()
309
- item["state"] = store.show(cluster["contract_id"], 1)["state"]
310
- except ContractError:
311
- pass
355
+ item = {"name": name, "role": cluster["role"], "created_at": cluster.get("created_at")}
356
+ if cluster["role"] == "owner":
357
+ members = []
358
+ for peer, member in sorted(cluster["members"].items()):
359
+ state = member.get("state")
360
+ if member.get("contract_id"):
361
+ try:
362
+ store = store or _contracts()
363
+ state = store.show(member["contract_id"], 1)["state"]
364
+ except ContractError:
365
+ pass
366
+ members.append({"deployment_id": peer, "display_name": member.get("display_name"),
367
+ "state": state, "joined_at": member.get("joined_at")})
368
+ item.update(members=members, open_invites=len(cluster["invites"]))
369
+ else:
370
+ state = cluster.get("state")
371
+ if cluster.get("contract_id"):
372
+ try:
373
+ store = store or _contracts()
374
+ state = store.show(cluster["contract_id"], 1)["state"]
375
+ except ContractError:
376
+ pass
377
+ item.update(peer=cluster["peer"], peer_name=cluster.get("peer_name"), state=state)
312
378
  out.append(item)
313
379
  return out
314
380
 
315
381
 
316
382
  # -----------------------------
317
- # Agent
383
+ # Agent: one pass per Global liveness tick
318
384
  # -----------------------------
319
385
 
320
- def _publish(cluster: dict) -> None:
321
- """Send our own signed events for the cluster's contract to the broker."""
386
+ def _publish(party: dict) -> None:
387
+ """Send our own signed events for this party's contract to the broker."""
388
+ if not party.get("contract_id"):
389
+ return
322
390
  store = _contracts()
323
- shown = store.show(cluster["contract_id"], 1, include_signed_events=True)
324
- done = set(cluster.setdefault("published", []))
391
+ shown = store.show(party["contract_id"], 1, include_signed_events=True)
392
+ done = set(party.setdefault("published", []))
325
393
  for event in shown["events"]:
326
394
  if event["actor_deployment_id"] != store.local_deployment_id or event["event_id"] in done:
327
395
  continue
@@ -329,75 +397,111 @@ def _publish(cluster: dict) -> None:
329
397
  "protocol_version": "2", "terms": shown["terms"], "signed_event": event["signed_event"],
330
398
  })
331
399
  done.add(event["event_id"])
332
- cluster["published"] = sorted(done)
400
+ party["published"] = sorted(done)
333
401
 
334
402
 
335
- def _sync_contract(cluster: dict, directory: list[dict]) -> str:
336
- """Import the peer's events for this cluster's contract; return the local state."""
403
+ def _sync_contract(party: dict, peer: str, directory: list[dict]) -> str:
404
+ """Import the peer's events for this party's contract; return the local state."""
337
405
  store = _contracts()
338
- if not cluster.get("contract_id"):
339
- # Member: the owner's proposal names us; adopt it.
406
+ if not party.get("contract_id"):
407
+ # Member: adopt the owner's proposal that names both of us -- the
408
+ # newest one that is not already revoked (an earlier cluster's may be).
409
+ candidates = []
340
410
  for item in directory:
341
411
  terms = item.get("terms") or {}
342
- if cluster.get("peer") in (terms.get("deployment_a"), terms.get("deployment_b")) and item["version"] == 1:
343
- cluster["contract_id"] = item["contract_id"]
344
- break
345
- else:
346
- return cluster["state"]
412
+ parties = {terms.get("deployment_a"), terms.get("deployment_b")}
413
+ if parties != {peer, store.local_deployment_id} or item["version"] != 1 or not item.get("events"):
414
+ continue
415
+ store.import_bundle(canonical_json({
416
+ "protocol_version": "2", "terms": terms, "events": item["events"],
417
+ }))
418
+ if store.show(item["contract_id"], 1)["state"] != "revoked":
419
+ candidates.append((terms["valid_from"], item["contract_id"]))
420
+ if not candidates:
421
+ return party["state"]
422
+ party["contract_id"] = max(candidates)[1]
347
423
  for item in directory:
348
- if item["contract_id"] == cluster["contract_id"] and item.get("terms"):
424
+ if item["contract_id"] == party["contract_id"] and item.get("terms"):
349
425
  store.import_bundle(canonical_json({
350
426
  "protocol_version": "2", "terms": item["terms"], "events": item["events"],
351
427
  }))
352
- return store.show(cluster["contract_id"], 1)["state"]
428
+ return store.show(party["contract_id"], 1)["state"]
429
+
430
+
431
+ def _advance(party: dict, peer: str, directory: list[dict], *, label: str, notify, is_member: bool) -> None:
432
+ """Move one bilateral contract one step; notify on active / revoked."""
433
+ before = party.get("state")
434
+ if before in ("pending", "revoked"):
435
+ return
436
+ state = _sync_contract(party, peer, directory)
437
+ store = _contracts()
438
+ if state == "proposed" and is_member:
439
+ store.accept(party["contract_id"], 1, actor_admin_ref="cluster")
440
+ state = store.activate(party["contract_id"], 1, actor_admin_ref="cluster")["state"]
441
+ elif state == "accepted":
442
+ mine = {e["actor_deployment_id"] for e in store.show(party["contract_id"], 1)["events"]
443
+ if e["event_type"] == "activate"}
444
+ if store.local_deployment_id not in mine:
445
+ state = store.activate(party["contract_id"], 1, actor_admin_ref="cluster")["state"]
446
+ _publish(party)
447
+ if party.get("contract_id"):
448
+ party["state"] = state
449
+ if state == "active" and before != "active":
450
+ notify(f"[cluster] {label} is active")
451
+ if state == "revoked" and before != "revoked":
452
+ notify(f"[cluster] {label} was ended by the other lab")
453
+
454
+
455
+ def _collect_joins(name: str, cluster: dict, broker_invites: list[dict], notify) -> None:
456
+ """Owner: turn redeemed invites into members; drop invites Global no longer holds."""
457
+ seen = {item["invite_id"]: item for item in broker_invites}
458
+ for invite_id, invite in list(cluster["invites"].items()):
459
+ item = seen.get(invite_id)
460
+ if item is None:
461
+ del cluster["invites"][invite_id] # expired or consumed elsewhere
462
+ continue
463
+ if not item.get("joiner"):
464
+ continue
465
+ joiner = item["joiner"]
466
+ secret = bytes.fromhex(invite["secret"])
467
+ if not hmac.compare_digest(joiner["tag"], _tag(secret, invite_id, joiner["identity"])):
468
+ notify(f"[cluster] {name}: a join arrived without the invite secret; ignored")
469
+ del cluster["invites"][invite_id]
470
+ continue
471
+ peer = joiner["identity"]["deployment_id"]
472
+ member = cluster["members"].setdefault(peer, {"published": [], "joined_at": int(time.time())})
473
+ member.update(display_name=joiner.get("display_name"), joiner=joiner["identity"], state="pending")
474
+ del cluster["invites"][invite_id]
475
+ if invite.get("auto", True):
476
+ _propose(cluster, member, peer)
477
+ else:
478
+ notify(f"[cluster] {peer} wants to join {name}: run `cluster accept {name}`")
353
479
 
354
480
 
355
481
  def tick(notify=None) -> None:
356
- """One agent pass: move every cluster one step closer to active."""
482
+ """One agent pass: move every cluster and member one step closer to active."""
357
483
  notify = notify or (lambda text: None)
358
484
  clusters = load()
359
485
  if not clusters:
360
486
  return
361
487
  directory = None
488
+ broker_invites = None
362
489
  for name, cluster in clusters.items():
363
- state = cluster.get("state")
364
- if state in ("created", "pending", "revoked"):
365
- continue
366
- if state == "invited":
367
- for item in g.call("GET", "/v2/broker/invites")["invites"]:
368
- if item["invite_id"] != cluster["invite_id"] or not item.get("joiner"):
369
- continue
370
- joiner = item["joiner"]
371
- secret = bytes.fromhex(cluster["secret"])
372
- if not hmac.compare_digest(joiner["tag"], _tag(secret, cluster["invite_id"], joiner["identity"])):
373
- notify(f"[cluster] {name}: a join arrived without the invite secret; ignored")
374
- continue
375
- cluster.update(state="pending", joiner=joiner["identity"])
490
+ if cluster["role"] == "owner":
491
+ if cluster["invites"]:
492
+ if broker_invites is None:
493
+ broker_invites = g.call("GET", "/v2/broker/invites")["invites"]
494
+ _collect_joins(name, cluster, broker_invites, notify)
376
495
  save(clusters)
377
- if cluster.get("auto"):
378
- accept(name)
379
- clusters = load()
380
- else:
381
- notify(f"[cluster] {joiner['identity']['deployment_id']} wants to join {name}: "
382
- f"run `cluster accept {name}`")
383
- continue
384
- if directory is None:
385
- directory = g.call("GET", "/v2/broker/contracts")["contracts"]
386
- contract_state = _sync_contract(cluster, directory)
387
- store = _contracts()
388
- if contract_state == "proposed" and cluster["role"] == "member":
389
- store.accept(cluster["contract_id"], 1, actor_admin_ref="cluster")
390
- contract_state = store.activate(cluster["contract_id"], 1, actor_admin_ref="cluster")["state"]
391
- elif contract_state == "accepted":
392
- mine = {e["actor_deployment_id"] for e in store.show(cluster["contract_id"], 1)["events"]
393
- if e["event_type"] == "activate"}
394
- if store.local_deployment_id not in mine:
395
- contract_state = store.activate(cluster["contract_id"], 1, actor_admin_ref="cluster")["state"]
396
- if cluster.get("contract_id"):
397
- _publish(cluster)
398
- if contract_state == "active" and state != "active":
399
- notify(f"[cluster] {name} is active")
400
- if contract_state == "revoked" and state != "revoked":
401
- notify(f"[cluster] {name} was ended by the other lab; `cluster remove {name}` forgets it")
402
- cluster["state"] = contract_state if cluster.get("contract_id") else state
496
+ if not cluster["members"]:
497
+ continue
498
+ if directory is None:
499
+ directory = g.call("GET", "/v2/broker/contracts")["contracts"]
500
+ for peer, member in cluster["members"].items():
501
+ _advance(member, peer, directory, label=f"{name}: {member.get('display_name') or peer}",
502
+ notify=notify, is_member=False)
503
+ else:
504
+ if directory is None:
505
+ directory = g.call("GET", "/v2/broker/contracts")["contracts"]
506
+ _advance(cluster, cluster["peer"], directory, label=name, notify=notify, is_member=True)
403
507
  save(clusters)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.0.dev28
3
+ Version: 1.1.0.dev30
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -52,7 +52,7 @@ remoteRF_server/host/host_tunnel_server.py,sha256=sz06xOtFbaDZSy1UQfw6faKhJmuOy5
52
52
  remoteRF_server/server/__init__.py,sha256=-1h_xQjiE66oiWRO-vnLGJ4smpgUNhFuV-ZreW9_tD4,667
53
53
  remoteRF_server/server/acc_perms.py,sha256=2loCvstVu8uvsb2uCv2Sk7iNQ5GyO_sBIkPQFzqOaRI,13334
54
54
  remoteRF_server/server/cert_provider.py,sha256=CRDrZL4DSf-eL6wzPJzwucrzPLOr2PvNRq5V--oZtr0,6272
55
- remoteRF_server/server/commands.py,sha256=9R-RlMRrUyZG0COydBThJcqJ2qNXdlGC4c-qh8hrAW8,17018
55
+ remoteRF_server/server/commands.py,sha256=dH19PK86cUiZhTnN9jC1RdM1t_t6dPuL6x-bdACxkSY,17008
56
56
  remoteRF_server/server/console.py,sha256=J91LfwPVmBgielOxfE740NOYduekggyZkqi-47y_A4w,4472
57
57
  remoteRF_server/server/control.py,sha256=Uq1Fk0zIoHHX_EXfz3OuNle8urvO_UhAiB5Lk5cs2e4,5177
58
58
  remoteRF_server/server/device_manager.py,sha256=nRTvk_jeZFU3QnlM6qf3DIckU2V2e_gyDeD5gW14KQI,38802
@@ -70,8 +70,8 @@ remoteRF_server/server/deployment/__init__.py,sha256=YV86uafd8XiGD7sLaqNfO5xxmt5
70
70
  remoteRF_server/server/deployment/capabilities.py,sha256=20RxmGG03VoQQZnV3jGhc-3i9B5hOl5fCopnsErFjPk,872
71
71
  remoteRF_server/server/federation/__init__.py,sha256=MZLqqh0Lzzh1t59VXrkS_bdpMO5znk1UVBjRGPvnUCg,735
72
72
  remoteRF_server/server/federation/admin.py,sha256=t3F17FIytekzclPICmaFJEA4-F5OuWcDx3rgmFga17A,9187
73
- remoteRF_server/server/federation/cli.py,sha256=TvklPVxG-6yPa0dUhiFo4tceGnrqe6euwKOHEz6NY3U,5952
74
- remoteRF_server/server/federation/cluster.py,sha256=mjp___eBcWwZK8xVi-2l_sX9dct1tHZrgqEZ5xugY0k,14933
73
+ remoteRF_server/server/federation/cli.py,sha256=Ax0tQw7_giLUP5lX1Y6x3sS1cbUbU2PxYur5k_5f3Po,7028
74
+ remoteRF_server/server/federation/cluster.py,sha256=2C8ys2BwUSxCLlYGvNjWU840q8e6H2ShGfl380yzA0I,19970
75
75
  remoteRF_server/server/federation/contracts.py,sha256=NYh7lb_9etJHFfzXwevjC4HfSBUypLxg5xGeBgydLF4,29305
76
76
  remoteRF_server/server/federation/destination.py,sha256=XawmGCiVbL_MfNNn8xwV9_bYxUDW1L7efCpL11K2JAc,35909
77
77
  remoteRF_server/server/federation/devices.py,sha256=n5MvnD0UZyl4sFpiekG9ProanWXTYizYq6YchCflLgY,18061
@@ -90,9 +90,9 @@ remoteRF_server/tools/gen_certs.py,sha256=Vbyu8JjRu__UYPVmyCRXt5SOw0Za1HV7g1md3G
90
90
  remoteRF_server/tools/gist_status.py,sha256=SFv2iQgQrIKT9gxKrU-_Dr7S0p0rE4wQM6J3OC6FcoM,4649
91
91
  remoteRF_server/tools/gist_status_testing.py,sha256=IYw7UkPjAUpKDTtIlnYGb5g6SJkwz4TRGUfJJ6f4LhQ,2690
92
92
  remoteRF_server/tools/introspect_uhd.py,sha256=vUpPb0rM9tsHPxWadS3EfMDE-gMd-TvNV0TjrZOQqBY,4531
93
- remoterf_server-1.1.0.dev28.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- remoterf_server-1.1.0.dev28.dist-info/METADATA,sha256=2nvyC9qOu-zwADY_pJnUNO6kvBTyKNq-SAwHgvoD-YE,42069
95
- remoterf_server-1.1.0.dev28.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
- remoterf_server-1.1.0.dev28.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
- remoterf_server-1.1.0.dev28.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
- remoterf_server-1.1.0.dev28.dist-info/RECORD,,
93
+ remoterf_server-1.1.0.dev30.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
+ remoterf_server-1.1.0.dev30.dist-info/METADATA,sha256=HAEJaoA51VNwScm3KxXvq650DbitxyOWXy1ChAXpD3s,42069
95
+ remoterf_server-1.1.0.dev30.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
+ remoterf_server-1.1.0.dev30.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
+ remoterf_server-1.1.0.dev30.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
+ remoterf_server-1.1.0.dev30.dist-info/RECORD,,