remoteRF-server 1.1.0.dev33__py3-none-any.whl → 1.1.0.dev35__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.
@@ -153,8 +153,8 @@ def _table() -> list[Command]:
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"),
155
155
  Command(("cluster remove",), "Leave or disband: cluster remove <name>", _federation("remove"), "Clusters"),
156
- Command(("cluster groups",), "Groups you share: cluster groups <name> [add|remove <group>]", _federation("groups"), "Clusters"),
157
- Command(("cluster codes",), "Student codes for a cluster: cluster codes <name> [count]", _federation("codes"), "Clusters"),
156
+ Command(("cluster groups",), "Choose which of your groups a cluster shares (interactive)", _federation("groups"), "Clusters"),
157
+ Command(("cluster codes",), "Codes for your users to join a cluster: cluster codes [count]", _federation("codes"), "Clusters"),
158
158
  Command(("cluster accept",), "", _federation("accept"), "Clusters", hidden=True),
159
159
  Command(("global cluster",), "", _federation("cluster"), "Global", hidden=True),
160
160
  # ---- gist ----
@@ -17,6 +17,7 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import re
20
21
  import sys
21
22
  from pathlib import Path
22
23
 
@@ -106,7 +107,7 @@ def _ask_for(verb: str) -> str:
106
107
  what = {"create": "Name for the new cluster (short slug, e.g. ucla-lab)",
107
108
  "invite": "Which cluster to invite a lab into",
108
109
  "accept": "Which cluster's pending join to accept",
109
- "groups": "Which cluster's shared groups to show",
110
+ "groups": "Which cluster to share groups with",
110
111
  "codes": "Which cluster to make student codes for",
111
112
  "remove": "Which cluster to leave (revokes the contract)"}[verb]
112
113
  if verb != "create" and names:
@@ -115,6 +116,40 @@ def _ask_for(verb: str) -> str:
115
116
  return _input(f"{what}{f' [{default}]' if default else ''}: ") or default
116
117
 
117
118
 
119
+ def _share_groups(name: str) -> int:
120
+ """Pick, by number, which of this server's groups the cluster shares.
121
+ Toggling: a shared group chosen again is unshared."""
122
+ from ...common.utils.ansi_codes import Sty
123
+ from ..user_group_handler import user_group_handler
124
+ from . import cluster
125
+
126
+ all_groups = user_group_handler.get_all_user_groups(include_users=False) or []
127
+ if not all_groups:
128
+ _print(" No user groups yet: make one with `groups create`, then share it here.", Sty.WARNING)
129
+ return 1
130
+ shared = {g["group_id"] for g in cluster.groups(name)}
131
+ print(f" Groups on this server (shared with {name} are marked):")
132
+ for index, group in enumerate(all_groups, 1):
133
+ mark = "*" if int(group["group_id"]) in shared else " "
134
+ devices = group.get("devices_whitelist") or []
135
+ print(f" {mark} {index:>2} {group['group_name']} ({len(devices)} device{'s' if len(devices) != 1 else ''})")
136
+ answer = _input("Toggle by number, e.g. 2 or 1,3 (Enter to leave as is): ")
137
+ picks = [part for part in re.split(r"[\s,]+", answer) if part]
138
+ if not picks:
139
+ return 0
140
+ for pick in picks:
141
+ if not pick.isdigit() or not 1 <= int(pick) <= len(all_groups):
142
+ _print(f" {pick} is not a number in the list.", Sty.WARNING)
143
+ continue
144
+ group = all_groups[int(pick) - 1]
145
+ if int(group["group_id"]) in shared:
146
+ _print(f" {cluster.detach_group(name, str(group['group_id']))} is no longer shared with {name}.", Sty.GRAY)
147
+ else:
148
+ _print(f" {cluster.attach_group(name, str(group['group_id']))} is now shared with {name}; "
149
+ "everyone in the cluster gets it.", Sty.GRAY)
150
+ return 0
151
+
152
+
118
153
  def _input(prompt: str) -> str:
119
154
  # The console's own prompt session: a bare input() inside the running
120
155
  # server fights prompt_toolkit for the terminal and kills the process.
@@ -128,7 +163,7 @@ def _input(prompt: str) -> str:
128
163
 
129
164
 
130
165
  _USAGE = ("Usage: cluster list | cluster create|invite|remove <name> | cluster join <code> | "
131
- "cluster groups <name> [add|remove <group>] | cluster codes <name> [count]")
166
+ "cluster groups [<name>] | cluster codes [<name>] [count]")
132
167
 
133
168
 
134
169
  def cmd_cluster(argv: list[str]) -> int:
@@ -140,6 +175,10 @@ def cmd_cluster(argv: list[str]) -> int:
140
175
  words = [a for a in argv[1:] if not a.startswith("--")]
141
176
  if verb == "list" and not words:
142
177
  return cmd_clusters()
178
+ if verb == "codes" and words and words[0].isdigit() and words[0] not in cluster.load():
179
+ words = [_ask_for(verb), words[0]] # `cluster codes 3`: the count came first
180
+ if not words[0]:
181
+ return 2
143
182
  if not words and verb in ("create", "invite", "accept", "join", "remove", "groups", "codes"):
144
183
  # Leaving the argument out asks for it (docs/deployment-groups.md §8).
145
184
  answer = _ask_for(verb)
@@ -167,10 +206,8 @@ def cmd_cluster(argv: list[str]) -> int:
167
206
  elif verb == "remove" and arg:
168
207
  _print(f" {arg}: {cluster.remove(arg)}.", Sty.GRAY)
169
208
  elif verb == "groups" and len(words) == 1:
170
- shared = cluster.groups(arg)
171
- _print(f" {arg} shares: " + (", ".join(g["group_name"] for g in shared) or "nothing yet"), Sty.GRAY)
172
- _print(f" Attach one with `cluster groups {arg} add <group>`; students of every lab in the "
173
- "cluster get it. Group names stay on this server.", Sty.GRAY)
209
+ cluster._find(cluster.load(), arg)
210
+ return _share_groups(arg)
174
211
  elif verb == "groups" and len(words) == 3 and words[1] in ("add", "remove"):
175
212
  if words[1] == "add":
176
213
  _print(f" {cluster.attach_group(arg, words[2])} is now shared with {arg}.", Sty.GRAY)
@@ -623,22 +623,28 @@ def _sync_contract(party: dict, peer: str, directory: list[dict]) -> str:
623
623
  parties = {terms.get("deployment_a"), terms.get("deployment_b")}
624
624
  if parties != {peer, store.local_deployment_id} or item["version"] != 1 or not item.get("events"):
625
625
  continue
626
- store.import_bundle(canonical_json({
627
- "protocol_version": "2", "terms": terms, "events": item["events"],
628
- }))
629
- if store.show(item["contract_id"], 1)["state"] != "revoked":
626
+ if _import(store, terms, item["events"]) != "revoked":
630
627
  candidates.append((terms["valid_from"], item["contract_id"]))
631
628
  if not candidates:
632
629
  return party["state"]
633
630
  party["contract_id"] = max(candidates)[1]
634
631
  for item in directory:
635
632
  if item["contract_id"] == party["contract_id"] and item.get("terms"):
636
- store.import_bundle(canonical_json({
637
- "protocol_version": "2", "terms": item["terms"], "events": item["events"],
638
- }))
633
+ _import(store, item["terms"], item["events"])
639
634
  return store.show(party["contract_id"], 1)["state"]
640
635
 
641
636
 
637
+ def _import(store: ContractStore, terms: dict, events: list) -> str:
638
+ """Import a directory entry; a contract already revoked here (an earlier
639
+ cluster between the same labs) is simply reported as such."""
640
+ try:
641
+ store.import_bundle(canonical_json({"protocol_version": "2", "terms": terms, "events": events}))
642
+ except ContractError as exc:
643
+ if exc.code not in ("contract_revoked", "contract_terminal", "invalid_transition"):
644
+ raise
645
+ return store.show(terms["contract_id"], 1)["state"]
646
+
647
+
642
648
  def _advance(party: dict, peer: str, directory: list[dict], *, label: str, notify, is_member: bool) -> None:
643
649
  """Move one bilateral contract one step; notify on active / revoked."""
644
650
  before = party.get("state")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.0.dev33
3
+ Version: 1.1.0.dev35
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=9ExGTi3nG7wqgSAyOEIFMDz0iBFLwC8QJdXrKNZNB5A,17277
55
+ remoteRF_server/server/commands.py,sha256=xG_RAYm0nHDqN74b-QPHizw2OodfrYAvdYlVNNi7tGE,17279
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=auqdGiGnEGIKQiXX2jFVt8iWmfyZ-V3w4dUmtWDKAZE,735
72
72
  remoteRF_server/server/federation/admin.py,sha256=t3F17FIytekzclPICmaFJEA4-F5OuWcDx3rgmFga17A,9187
73
- remoteRF_server/server/federation/cli.py,sha256=nFlUGmhpndw0RafjeRQjz7cYieKtKy7U60_n738g-s0,9021
74
- remoteRF_server/server/federation/cluster.py,sha256=dsraUf7wKG4GdqcyhdO195v1ypwSRpFcJdyY5uatHCM,32932
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
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.dev33.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- remoterf_server-1.1.0.dev33.dist-info/METADATA,sha256=4m84njQmG2sgUH0guTvQ-t5pjjbnq7nrcPj7EMKqxmU,42069
95
- remoterf_server-1.1.0.dev33.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
- remoterf_server-1.1.0.dev33.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
- remoterf_server-1.1.0.dev33.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
- remoterf_server-1.1.0.dev33.dist-info/RECORD,,
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,,