remoteRF-server 1.1.0.dev35__py3-none-any.whl → 1.1.0.dev36__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.
@@ -38,6 +38,10 @@ def _lab(party: dict) -> str:
38
38
  return f"{party['display_name']} ({short}…)" if party.get("display_name") else party["deployment_id"]
39
39
 
40
40
 
41
+ def _n(count: int, noun: str) -> str:
42
+ return f"{count} {noun}{'' if count == 1 else 's'}"
43
+
44
+
41
45
  def _day(epoch) -> str:
42
46
  import datetime
43
47
 
@@ -64,6 +68,7 @@ def cmd_clusters() -> int:
64
68
  for index, item in enumerate(items):
65
69
  if index:
66
70
  rows.append("")
71
+ owner_row = None
67
72
  if item["role"] == "owner":
68
73
  invites = item["open_invites"]
69
74
  rows.append(f"{item['name']} (yours)" + (f" {invites} open invite{'s' if invites != 1 else ''}" if invites else ""))
@@ -72,20 +77,25 @@ def cmd_clusters() -> int:
72
77
  if not item["members"] and not invites:
73
78
  rows.append(f" nobody yet: `cluster invite {item['name']}`")
74
79
  else:
75
- owner = {"display_name": item.get("peer_name"), "deployment_id": item["peer"]}
80
+ owner_row = owner = {"display_name": item.get("peer_name"), "deployment_id": item["peer"]}
76
81
  rows.append(f"{item['name']} {item['state']}")
77
82
  labs = [(owner, "admin", "", None), (me, "member", "", None)] + [
78
83
  (p, "member", p["state"] or "joining", None) for p in item.get("peers", [])]
79
84
  for lab, role, lab_state, since in labs:
80
- you = " (you)" if lab["deployment_id"] == me["deployment_id"] else ""
81
- line = f" {_lab(lab)} {role}{you}"
85
+ you = lab["deployment_id"] == me["deployment_id"]
86
+ line = f" {_lab(lab)} {role}" + (" (you)" if you else "")
82
87
  if lab_state and lab_state != "active":
83
88
  line += f" {lab_state}"
84
89
  if since:
85
90
  line += f" since {_day(since)}"
86
91
  rows.append(line)
87
- if item["groups"]:
88
- rows.append(" sharing: " + ", ".join(group["group_name"] for group in item["groups"]))
92
+ if you:
93
+ rows.append(" sharing: " + (", ".join(
94
+ f"{g['group_name']} ({_n(len(g['devices']), 'device')})" for g in item["groups"]) or "nothing"))
95
+ else:
96
+ sharing = lab.get("sharing") if lab is not owner_row else item.get("peer_sharing")
97
+ if sharing:
98
+ rows.append(f" sharing {_n(sharing['groups'], 'group')}, {_n(sharing['devices'], 'device')}")
89
99
  rows.append(f" {item['students']} of your users enrolled, {item['visitors']} visiting")
90
100
  print()
91
101
  print_panel("Clusters", rows, literal=True)
@@ -404,7 +404,8 @@ def list_clusters() -> list[dict]:
404
404
  except ContractError:
405
405
  pass
406
406
  members.append({"deployment_id": peer, "display_name": member.get("display_name"),
407
- "state": state, "joined_at": member.get("joined_at")})
407
+ "state": state, "joined_at": member.get("joined_at"),
408
+ "sharing": member.get("sharing")})
408
409
  item.update(members=members, open_invites=len(cluster["invites"]))
409
410
  else:
410
411
  state = cluster.get("state")
@@ -414,9 +415,11 @@ def list_clusters() -> list[dict]:
414
415
  state = store.show(cluster["contract_id"], 1)["state"]
415
416
  except ContractError:
416
417
  pass
417
- peers = [{"deployment_id": peer, "display_name": party.get("display_name"), "state": party.get("state")}
418
+ peers = [{"deployment_id": peer, "display_name": party.get("display_name"), "state": party.get("state"),
419
+ "sharing": party.get("sharing")}
418
420
  for peer, party in sorted(cluster.get("peers", {}).items())]
419
- item.update(peer=cluster["peer"], peer_name=cluster.get("peer_name"), state=state, peers=peers)
421
+ item.update(peer=cluster["peer"], peer_name=cluster.get("peer_name"), state=state, peers=peers,
422
+ peer_sharing=cluster.get("sharing"))
420
423
  item.update(groups=groups(name), **_account_counts(name))
421
424
  out.append(item)
422
425
  return out
@@ -445,12 +448,23 @@ def groups(name: str) -> list[dict]:
445
448
  out = []
446
449
  with _usergroups() as conn:
447
450
  for group_id in cluster.get("groups", []):
448
- row = conn.execute("SELECT group_name FROM UserGroups WHERE group_id=?", (group_id,)).fetchone()
451
+ row = conn.execute("SELECT group_name,devices_whitelist FROM UserGroups WHERE group_id=?",
452
+ (group_id,)).fetchone()
449
453
  if row is not None:
450
- out.append({"group_id": group_id, "group_name": row[0]})
454
+ try:
455
+ devices = {int(d) for d in json.loads(row[1] or "[]")}
456
+ except (ValueError, TypeError):
457
+ devices = set()
458
+ out.append({"group_id": group_id, "group_name": row[0], "devices": sorted(devices)})
451
459
  return out
452
460
 
453
461
 
462
+ def _sharing(name: str) -> dict:
463
+ """Counts only -- all a partner ever learns about what we share."""
464
+ shared = groups(name)
465
+ return {"groups": len(shared), "devices": len({d for g in shared for d in g["devices"]})}
466
+
467
+
454
468
  def _account_counts(name: str) -> dict:
455
469
  with _usergroups() as conn:
456
470
  rows = dict(conn.execute(
@@ -687,6 +701,32 @@ def _publish_roster(name: str, cluster: dict) -> None:
687
701
  cluster["roster"] = active
688
702
 
689
703
 
704
+ def _publish_status(name: str, cluster: dict) -> None:
705
+ """Any lab: post our counts for this cluster when they change."""
706
+ sharing = _sharing(name)
707
+ if sharing == cluster.get("status"):
708
+ return
709
+ key, _, me = g._signing_material()
710
+ owner = me if cluster["role"] == "owner" else cluster["peer"]
711
+ status = {"cluster_name": name, "owner": owner, "deployment_id": me, "issued_at": int(time.time()), **sharing}
712
+ signature = base64.b64encode(key.sign(canonical_json(status).encode())).decode("ascii")
713
+ g.call("POST", "/v2/broker/clusters/roster", {"protocol_version": "2", "status": status, "signature": signature})
714
+ cluster["status"] = sharing
715
+
716
+
717
+ def _take_statuses(name: str, cluster: dict, statuses: list[dict]) -> None:
718
+ """Attach each partner's verified counts to its row."""
719
+ owner = g._signing_material()[2] if cluster["role"] == "owner" else cluster["peer"]
720
+ parties = dict(_parties(cluster))
721
+ for item in statuses:
722
+ status = item.get("status") or {}
723
+ peer = status.get("deployment_id")
724
+ if status.get("owner") != owner or status.get("cluster_name") != name or peer not in parties:
725
+ continue
726
+ if _roster_signed_by(peer, status, item.get("signature", "")):
727
+ parties[peer]["sharing"] = {"groups": status["groups"], "devices": status["devices"]}
728
+
729
+
690
730
  def _roster_signed_by(owner: str, roster: dict, signature: str) -> bool:
691
731
  from cryptography.exceptions import InvalidSignature
692
732
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
@@ -774,7 +814,7 @@ def tick(notify=None, home=None) -> None:
774
814
  return
775
815
  directory = None
776
816
  broker_invites = None
777
- rosters = None
817
+ board = None # rosters and status notes, one GET for all clusters
778
818
  for name, cluster in clusters.items():
779
819
  if cluster["role"] == "owner":
780
820
  if cluster["invites"]:
@@ -797,9 +837,15 @@ def tick(notify=None, home=None) -> None:
797
837
  if directory is None:
798
838
  directory = g.call("GET", "/v2/broker/contracts")["contracts"]
799
839
  _advance(cluster, cluster["peer"], directory, label=name, notify=notify, is_member=True)
800
- if rosters is None:
801
- rosters = g.call("GET", "/v2/broker/clusters/roster")["rosters"]
802
- _mesh(name, cluster, rosters, directory, notify)
840
+ if board is None:
841
+ board = g.call("GET", "/v2/broker/clusters/roster")
842
+ if cluster["role"] != "owner":
843
+ _mesh(name, cluster, board["rosters"], directory, notify)
844
+ try:
845
+ _publish_status(name, cluster)
846
+ except g.GlobalError as exc:
847
+ notify(f"[cluster] {name}: could not publish what we share: {exc}")
848
+ _take_statuses(name, cluster, board.get("statuses", []))
803
849
  save(clusters)
804
850
  if home is not None:
805
851
  enroll_students(home, notify)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.0.dev35
3
+ Version: 1.1.0.dev36
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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=auqdGiGnEGIKQiXX2jFVt8iWmfyZ-V3w4dUmtWDKAZE,735
72
72
  remoteRF_server/server/federation/admin.py,sha256=t3F17FIytekzclPICmaFJEA4-F5OuWcDx3rgmFga17A,9187
73
- remoteRF_server/server/federation/cli.py,sha256=m3fzURqmnZH1pO7wHyqg5EUoo4ZPiw7P3K_9q8SXQu8,10703
74
- remoteRF_server/server/federation/cluster.py,sha256=yKGFRAI-RHB_xZivJkxaK_Fniz2XGRUfArJXM_DH2WQ,33207
73
+ remoteRF_server/server/federation/cli.py,sha256=iZpS-31516atRauI6zXdCTw81hV7QNEdyDqC8ijXoEA,11175
74
+ remoteRF_server/server/federation/cluster.py,sha256=m_SW942R7d0a8X16kMfKn3fdUbyJsAmonI67Wl8OIOs,35529
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.dev35.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- remoterf_server-1.1.0.dev35.dist-info/METADATA,sha256=meRywJNeTPy2Yol5Gc-G6zGPXnvPdyIkH-JlIe9H93Y,42069
95
- remoterf_server-1.1.0.dev35.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
- remoterf_server-1.1.0.dev35.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
- remoterf_server-1.1.0.dev35.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
- remoterf_server-1.1.0.dev35.dist-info/RECORD,,
93
+ remoterf_server-1.1.0.dev36.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
+ remoterf_server-1.1.0.dev36.dist-info/METADATA,sha256=PA_v4CEjH9HfN0EABXND3J1B53HdPVIhfxZhxwQ_vvc,42069
95
+ remoterf_server-1.1.0.dev36.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
+ remoterf_server-1.1.0.dev36.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
+ remoterf_server-1.1.0.dev36.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
+ remoterf_server-1.1.0.dev36.dist-info/RECORD,,