remoteRF-server 1.1.0.dev38__py3-none-any.whl → 1.1.0.dev40__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.
@@ -89,7 +89,7 @@ def _global(verb: str):
89
89
 
90
90
 
91
91
  def _federation(verb: str):
92
- return lambda argv: _shell("cmd_federation")([verb, *argv])
92
+ return lambda argv: _shell("cmd_federation")([*verb.split(), *argv])
93
93
 
94
94
 
95
95
  def _table() -> list[Command]:
@@ -155,7 +155,8 @@ def _table() -> list[Command]:
155
155
  Command(("cluster join",), "Join with a code: cluster join <code>", _federation("join"), "Clusters"),
156
156
  Command(("cluster remove",), "Leave or disband: cluster remove <name>", _federation("remove"), "Clusters"),
157
157
  Command(("cluster groups",), "Choose which of your groups a cluster shares (interactive)", _federation("groups"), "Clusters"),
158
- Command(("cluster codes",), "Codes for your users to join a cluster: cluster codes [count]", _federation("codes"), "Clusters"),
158
+ Command(("cluster codes create",), "Codes for your users to join a cluster (interactive)", _federation("codes create"), "Clusters"),
159
+ Command(("cluster codes list",), "List cluster codes", _federation("codes list"), "Clusters"),
159
160
  Command(("cluster accept",), "", _federation("accept"), "Clusters", hidden=True),
160
161
  Command(("global cluster",), "", _federation("cluster"), "Global", hidden=True),
161
162
  # ---- gist ----
@@ -191,7 +192,7 @@ def normalize(argv: Iterable[str]) -> list[str]:
191
192
  def resolve(argv: list[str]) -> tuple[Optional[Command], list[str]]:
192
193
  """Longest phrase match wins: `devices add pluto` → ("devices add", ["pluto"])."""
193
194
  words = normalize(argv)
194
- for length in (2, 1):
195
+ for length in (3, 2, 1):
195
196
  phrase = " ".join(words[:length]).lower()
196
197
  for command in commands():
197
198
  if phrase in command.names:
@@ -170,7 +170,7 @@ def _ask_for(verb: str) -> str:
170
170
  "accept": "Which cluster's pending join to accept",
171
171
  "groups": "Which cluster to share groups with",
172
172
  "info": "Which cluster to show",
173
- "codes": "Which cluster to make student codes for",
173
+ "codes": "Which cluster to make codes for",
174
174
  "remove": "Which cluster to leave (revokes the contract)"}[verb]
175
175
  if verb != "create" and names:
176
176
  print(f" Your clusters: {', '.join(names)}")
@@ -212,6 +212,72 @@ def _share_groups(name: str) -> int:
212
212
  return 0
213
213
 
214
214
 
215
+ def _codes_create(name: str) -> int:
216
+ """The `codes create` questions, bound to a cluster instead of a group."""
217
+ from ...common.utils.ansi_codes import Sty
218
+ from ...common.utils.panel import print_table
219
+ from .. import global_onboarding as g
220
+ from . import cluster
221
+
222
+ if not name:
223
+ return 2
224
+ cluster._find(cluster.load(), name)
225
+
226
+ def ask_int(prompt: str, default: int, minimum: int) -> int | None:
227
+ while True:
228
+ answer = _input(f"{prompt} [{default}] (c to cancel): ")
229
+ if answer.lower() in ("c", "q", "cancel"):
230
+ return None
231
+ if not answer:
232
+ return default
233
+ if answer.isdigit() and int(answer) >= minimum:
234
+ return int(answer)
235
+ _print(f" Enter a whole number of at least {minimum}.", Sty.WARNING)
236
+
237
+ count = ask_int("How many codes?", 1, 1)
238
+ uses = None if count is None else ask_int("Uses per code?", 1, 1)
239
+ duration = None if uses is None else ask_int(
240
+ "Duration in seconds? (0=never expires)", cluster.CODE_DAYS * 86400, 0)
241
+ if duration is None:
242
+ return 2
243
+ codes = cluster.mint_codes(name, count=count, uses=uses, duration_sec=duration)
244
+ rows = [{"invite": g.invite(code)} for code in codes]
245
+ if rows[0]["invite"] == codes[0]:
246
+ print(f"Hand these out as is; users connect first with: remoterf -c -a {g.lan_endpoint() or '<lan address>'}")
247
+ else:
248
+ print("Hand these out as is; new users enter one at: remoterf --register, "
249
+ "existing users enroll with it in the Client.")
250
+ print_table(f"Cluster {name} codes", rows, ["invite"])
251
+ return 0
252
+
253
+
254
+ def _codes_list(name: str | None) -> int:
255
+ from ...common.utils.panel import print_panel
256
+ from .. import global_onboarding as g
257
+ from ..user_group_cli import _fmt_duration
258
+ from . import cluster
259
+
260
+ if name:
261
+ cluster._find(cluster.load(), name)
262
+ rows = []
263
+ codes = cluster.list_codes(name)
264
+ if not codes:
265
+ rows.append("No cluster codes. Make some with `cluster codes create`.")
266
+ current = None
267
+ for code in codes:
268
+ if code["cluster_name"] != current:
269
+ if current is not None:
270
+ rows.append("")
271
+ current = code["cluster_name"]
272
+ rows.append(current)
273
+ rows.append(f" {g.invite(code['code_name'])} · {_n(code['uses'], 'use')} left · "
274
+ f"{_fmt_duration(code['duration_sec'])}")
275
+ print()
276
+ print_panel("Cluster codes", rows, literal=True)
277
+ print()
278
+ return 0
279
+
280
+
215
281
  def _input(prompt: str) -> str:
216
282
  # The console's own prompt session: a bare input() inside the running
217
283
  # server fights prompt_toolkit for the terminal and kills the process.
@@ -225,7 +291,7 @@ def _input(prompt: str) -> str:
225
291
 
226
292
 
227
293
  _USAGE = ("Usage: cluster list | cluster create|invite|remove <name> | cluster join <code> | "
228
- "cluster info [<name>] | cluster groups [<name>] | cluster codes [<name>] [count]")
294
+ "cluster info [<name>] | cluster groups [<name>] | cluster codes create|list [<name>]")
229
295
 
230
296
 
231
297
  def cmd_cluster(argv: list[str]) -> int:
@@ -237,11 +303,10 @@ def cmd_cluster(argv: list[str]) -> int:
237
303
  words = [a for a in argv[1:] if not a.startswith("--")]
238
304
  if verb == "list" and not words:
239
305
  return cmd_clusters()
240
- if verb == "codes" and words and words[0].isdigit() and words[0] not in cluster.load():
241
- words = [_ask_for(verb), words[0]] # `cluster codes 3`: the count came first
242
- if not words[0]:
243
- return 2
244
- if not words and verb in ("create", "invite", "accept", "join", "remove", "groups", "codes", "info"):
306
+ if verb == "codes" and (not words or words[0] not in ("create", "list")):
307
+ print("Usage: cluster codes create [<cluster>] | cluster codes list [<cluster>]", file=sys.stderr)
308
+ return 2
309
+ if not words and verb in ("create", "invite", "accept", "join", "remove", "groups", "info"):
245
310
  # Leaving the argument out asks for it (docs/deployment-groups.md §8).
246
311
  answer = _ask_for(verb)
247
312
  if not answer:
@@ -277,13 +342,10 @@ def cmd_cluster(argv: list[str]) -> int:
277
342
  _print(f" {cluster.attach_group(arg, words[2])} is now shared with {arg}.", Sty.GRAY)
278
343
  else:
279
344
  _print(f" {cluster.detach_group(arg, words[2])} is no longer shared with {arg}.", Sty.GRAY)
280
- elif verb == "codes" and arg:
281
- count = int(words[1]) if len(words) > 1 else 1
282
- codes = cluster.mint_codes(arg, count=count)
283
- _print(f" Give each of your users one code; they enter it in the Client like any enrollment "
284
- f"code (single use, {cluster.CODE_DAYS} days):", Sty.GRAY)
285
- for code in codes:
286
- _print(f" {code}", Sty.BOLD)
345
+ elif verb == "codes" and words[0] == "create":
346
+ return _codes_create(words[1] if len(words) > 1 else _ask_for("codes"))
347
+ elif verb == "codes" and words[0] == "list":
348
+ return _codes_list(words[1] if len(words) > 1 else None)
287
349
  else:
288
350
  print(_USAGE, file=sys.stderr)
289
351
  return 2
@@ -491,15 +491,33 @@ def attach_group(name: str, ref: str) -> str:
491
491
  if group_id in cluster.get("groups", []):
492
492
  raise ClusterError(f"{group_name} is already attached to {name}")
493
493
  cluster.setdefault("groups", []).append(group_id)
494
- conn.execute(
495
- "INSERT OR IGNORE INTO UsersLookup(user_id,group_id) "
496
- "SELECT local_account_id,? FROM ClusterAccounts WHERE cluster_name=?",
497
- (group_id, name),
498
- )
494
+ for (account,) in conn.execute(
495
+ "SELECT local_account_id FROM ClusterAccounts WHERE cluster_name=?", (name,)).fetchall():
496
+ _grant(conn, name, account, group_id)
499
497
  save(clusters)
500
498
  return group_name
501
499
 
502
500
 
501
+ def _grant(conn, name: str, account: int, group_id: int) -> None:
502
+ """Add a membership on the cluster's behalf -- remembered only if it was
503
+ the cluster, not a group code, that granted it."""
504
+ added = conn.execute("INSERT OR IGNORE INTO UsersLookup(user_id,group_id) VALUES (?,?)",
505
+ (int(account), int(group_id))).rowcount
506
+ if added:
507
+ conn.execute("INSERT OR IGNORE INTO ClusterMemberships(cluster_name,local_account_id,group_id) "
508
+ "VALUES (?,?,?)", (name, int(account), int(group_id)))
509
+
510
+
511
+ def _take_back(conn, name: str, group_id: int | None = None) -> None:
512
+ """Remove exactly the memberships this cluster granted (one group, or all)."""
513
+ where = "cluster_name=?" + ("" if group_id is None else " AND group_id=?")
514
+ args = (name,) if group_id is None else (name, int(group_id))
515
+ conn.execute(
516
+ f"DELETE FROM UsersLookup WHERE (user_id,group_id) IN "
517
+ f"(SELECT local_account_id,group_id FROM ClusterMemberships WHERE {where})", args)
518
+ conn.execute(f"DELETE FROM ClusterMemberships WHERE {where}", args)
519
+
520
+
503
521
  def detach_group(name: str, ref: str) -> str:
504
522
  clusters = load()
505
523
  cluster = _find(clusters, name)
@@ -508,23 +526,14 @@ def detach_group(name: str, ref: str) -> str:
508
526
  if group_id not in cluster.get("groups", []):
509
527
  raise ClusterError(f"{group_name} is not attached to {name}")
510
528
  cluster["groups"].remove(group_id)
511
- conn.execute(
512
- "DELETE FROM UsersLookup WHERE group_id=? AND user_id IN "
513
- "(SELECT local_account_id FROM ClusterAccounts WHERE cluster_name=?)",
514
- (group_id, name),
515
- )
529
+ _take_back(conn, name, group_id)
516
530
  save(clusters)
517
531
  return group_name
518
532
 
519
533
 
520
534
  def _forget_accounts(name: str, cluster: dict) -> None:
521
535
  with _usergroups() as conn:
522
- for group_id in cluster.get("groups", []):
523
- conn.execute(
524
- "DELETE FROM UsersLookup WHERE group_id=? AND user_id IN "
525
- "(SELECT local_account_id FROM ClusterAccounts WHERE cluster_name=?)",
526
- (group_id, name),
527
- )
536
+ _take_back(conn, name)
528
537
  conn.execute("DELETE FROM ClusterAccounts WHERE cluster_name=?", (name,))
529
538
 
530
539
 
@@ -536,24 +545,35 @@ def join_account(conn, name: str, cluster: dict, local_account_id: int, *, kind:
536
545
  (name, int(local_account_id), kind, int(time.time())),
537
546
  )
538
547
  for group_id in cluster.get("groups", []):
539
- conn.execute("INSERT OR IGNORE INTO UsersLookup(user_id,group_id) VALUES (?,?)",
540
- (int(local_account_id), int(group_id)))
548
+ _grant(conn, name, local_account_id, group_id)
541
549
 
542
550
 
543
- def mint_codes(name: str, *, count: int = 1, uses: int = 1, days: int = CODE_DAYS) -> list[str]:
551
+ def mint_codes(name: str, *, count: int = 1, uses: int = 1, duration_sec: int = CODE_DAYS * 86400) -> list[str]:
552
+ """Same shape as a group code (uses, duration; 0 = never expires), bound
553
+ to the cluster instead of a group."""
544
554
  _find(load(), name)
545
- if not 1 <= count <= 200 or uses < 1 or days < 1:
546
- raise ClusterError("count must be 1-200, uses and days at least 1")
555
+ if not 1 <= count <= 200 or uses < 1 or duration_sec < 0:
556
+ raise ClusterError("count must be 1-200, uses at least 1, duration 0 or more seconds")
547
557
  codes = [CODE_PREFIX + secrets.token_urlsafe(18) for _ in range(count)]
548
558
  with _usergroups() as conn:
549
559
  conn.executemany(
550
560
  "INSERT INTO EnrollmentCodes(code_name,group_id,uses,duration_sec,created_at,cluster_name) "
551
561
  "VALUES (?,-1,?,?,?,?)",
552
- [(code, uses, days * 86400, datetime.now().isoformat(sep=" "), name) for code in codes],
562
+ [(code, uses, duration_sec, datetime.now().isoformat(sep=" "), name) for code in codes],
553
563
  )
554
564
  return codes
555
565
 
556
566
 
567
+ def list_codes(name: str | None = None) -> list[dict]:
568
+ with _usergroups() as conn:
569
+ rows = conn.execute(
570
+ "SELECT code_name,cluster_name,uses,duration_sec FROM EnrollmentCodes WHERE cluster_name IS NOT NULL"
571
+ + (" AND cluster_name=?" if name else "") + " ORDER BY cluster_name,code_id",
572
+ (name,) if name else (),
573
+ ).fetchall()
574
+ return [{"code_name": r[0], "cluster_name": r[1], "uses": int(r[2]), "duration_sec": int(r[3])} for r in rows]
575
+
576
+
557
577
  def redeem_code(local_account_id: int, code_name: str) -> Optional[str]:
558
578
  """A student types a cluster code at home: returns the cluster joined, or None."""
559
579
  if not code_name.startswith(CODE_PREFIX):
@@ -778,6 +778,11 @@ def migrate_gate_f_usergroups_schema(conn: sqlite3.Connection) -> None:
778
778
  "cluster_name TEXT NOT NULL, local_account_id INTEGER NOT NULL, "
779
779
  "kind TEXT NOT NULL CHECK(kind IN ('local','proxy')), joined_at INTEGER NOT NULL, "
780
780
  "PRIMARY KEY(cluster_name,local_account_id));"
781
+ # Memberships the cluster itself created, so un-sharing takes back
782
+ # only those and never a membership a group code granted.
783
+ "CREATE TABLE IF NOT EXISTS ClusterMemberships ("
784
+ "cluster_name TEXT NOT NULL, local_account_id INTEGER NOT NULL, group_id INTEGER NOT NULL, "
785
+ "PRIMARY KEY(cluster_name,local_account_id,group_id));"
781
786
  )
782
787
  conn.executescript(_GATE_F_DDL)
783
788
  conn.execute(
@@ -428,7 +428,16 @@ class ReservationHandler:
428
428
  "VALUES (?, ?, 'active', ?)",
429
429
  (uid, str(uuid4()), int(time.time())),
430
430
  )
431
- user_group_handler.enroll_user_with_code(uid=uid, code_name=ec)
431
+ if ec.startswith("cluster."):
432
+ from .federation import cluster as cluster_module
433
+
434
+ if cluster_module.redeem_code(uid, ec) is not None:
435
+ try:
436
+ cluster_module.enroll_students(self, only_account=uid)
437
+ except Exception: # noqa: BLE001 - the tick retries
438
+ pass
439
+ else:
440
+ user_group_handler.enroll_user_with_code(uid=uid, code_name=ec)
432
441
  perms.set_user(un) # set perms
433
442
  return {"UC": map_arg(f'{un}')}
434
443
  except Exception as e:
@@ -37,7 +37,7 @@ def _invites(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
37
37
  code = str(row.get("code_name", ""))
38
38
  # A federation-bound code is redeemed through its signed invitation,
39
39
  # never typed as a Global invite.
40
- federated = row.get("federation_contract_id") is not None or row.get("cluster_name")
40
+ federated = row.get("federation_contract_id") is not None
41
41
  row["invite"] = code if federated else invite(
42
42
  code, lan_only=lan_only.get(int(row.get("group_id", -1)), False)
43
43
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.1.0.dev38
3
+ Version: 1.1.0.dev40
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=6HcvEpznOblV57NMuOJTl2VY2js1wbuLV9mwxGDqMeg,17399
55
+ remoteRF_server/server/commands.py,sha256=ccb_Ab1mbIkosBsFGJnDSccrjWtR_Lymvvn3IqhOnyc,17519
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=PPuCwlPSmjfVpCqy_hCaOr9WvFgE8mqtLMhWOAgXNDA,39281
@@ -61,17 +61,17 @@ remoteRF_server/server/dynamic_v2.py,sha256=AMhQEFhNAvCNcF-PdfCCQQLmeoaR7s1Os8et
61
61
  remoteRF_server/server/global_onboarding.py,sha256=boLAKXCKvcsIY3TDiFwduMaHdDMB9Cr104MyWJYZ-BA,39891
62
62
  remoteRF_server/server/grpc_server.py,sha256=91FpJubzN_Wfuk6fW_owIEzNCukd333sAGW68FUU8pk,46787
63
63
  remoteRF_server/server/host_dynamic_v2.py,sha256=1rJQwP4knSd0EXkByrSn_ryjLx81f6sp6SNYr_aEass,8105
64
- remoteRF_server/server/reservation.py,sha256=83rEWlfKauxqDOgv2IdDa8JBZWsLf_l0v1sEv9xl1RM,67411
64
+ remoteRF_server/server/reservation.py,sha256=BQJLGNVAdKMZu2Lg4DGO9UhuJ0b1GSF8K1DgIOFrYUg,67816
65
65
  remoteRF_server/server/rpc_manager.py,sha256=BDvsTSVRxwNF2ujuMbIb5TYNAUI6AH4xXBuXrb8lltM,17223
66
- remoteRF_server/server/user_group_cli.py,sha256=6zNquuedH_q6pwq4trzRkEfE0wzAe7-RkFlGMWR-ZnM,25072
66
+ remoteRF_server/server/user_group_cli.py,sha256=thg-pPIqfWVi2ZfvRwJuR2uFO3cJZIbsNLtquJrGtH8,25045
67
67
  remoteRF_server/server/user_group_handler.py,sha256=1ax73W_P3H6GDxQS13evwA7G7skIElFVLuS0MFqsFyU,41780
68
68
  remoteRF_server/server/v2_errors.py,sha256=IS3hIhf5tDe7jaOrDAVQPjGBpqAjN7442BlJYgfJo7g,3331
69
69
  remoteRF_server/server/deployment/__init__.py,sha256=YV86uafd8XiGD7sLaqNfO5xxmt5te2R6DUhD-w1lZuY,77
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=Hgn0FyDwB57QDB5AdIBJOcQKQAS7G4BkKRa_QHMrwlo,13990
74
- remoteRF_server/server/federation/cluster.py,sha256=bQpT7jq-2d87ae6AkmsKktozBsULSy3QrIbAVgBLMQg,36238
73
+ remoteRF_server/server/federation/cli.py,sha256=pJh2TyEqOcxO4uBEDpQsDwvM0CLf7IdpSHAaZrAvt2E,16348
74
+ remoteRF_server/server/federation/cluster.py,sha256=SvJVe1EGKWjmIf8uPTLgSWlG6Hax7gh_RUmWLmabicM,37451
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
@@ -80,7 +80,7 @@ remoteRF_server/server/federation/home.py,sha256=-_0__wJN50Y1-Dd396_iUZjGESBkoBN
80
80
  remoteRF_server/server/federation/home_devices.py,sha256=lbsw201HBOYPP5vbN172vdm2O_ouCdZBo1cRYCVScmg,18754
81
81
  remoteRF_server/server/federation/identity.py,sha256=UtQJKm2mChWZto0ZCg385k_ObrwOUtxY7XFyU_s5Q10,7436
82
82
  remoteRF_server/server/federation/legacy_state.py,sha256=SpeTZHFqymNYGWG7A3B-78dIRIVLYltBbE2zGDWzt-Q,19838
83
- remoteRF_server/server/federation/migrations.py,sha256=wClgRrwelcqHebSLJEDQR98WWUgzaEMrio8DLVOfcdA,50594
83
+ remoteRF_server/server/federation/migrations.py,sha256=opPIfoNvR3BwoN9BaZsuEs14cT-zz4ghXWHV0hno7QI,50958
84
84
  remoteRF_server/server/federation/permissions.py,sha256=ou4uFsMAGHnDeBW6zbpGeUTwp0thlYXT5nIAgd3B1rg,15861
85
85
  remoteRF_server/server/federation/principals.py,sha256=CsRYa8POmtMaxKzAGwY14sxBTNS5uKjM1nUE19mjpoQ,10410
86
86
  remoteRF_server/server/federation/runtime.py,sha256=5CoDlu0jSQrFdwRoGL2Z4_CWxnA46B3j6-eNQo8N2FI,7624
@@ -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.dev38.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- remoterf_server-1.1.0.dev38.dist-info/METADATA,sha256=fexWF3tdfTpy7t1qFRAqEh2YudcllkB-HEOel1g1_6M,42069
95
- remoterf_server-1.1.0.dev38.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
- remoterf_server-1.1.0.dev38.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
- remoterf_server-1.1.0.dev38.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
- remoterf_server-1.1.0.dev38.dist-info/RECORD,,
93
+ remoterf_server-1.1.0.dev40.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
+ remoterf_server-1.1.0.dev40.dist-info/METADATA,sha256=aX2kuvEg8wV0m1NMsKDFMpOzE_nOALZpwgiJYCPZsF0,42069
95
+ remoterf_server-1.1.0.dev40.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
96
+ remoterf_server-1.1.0.dev40.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
97
+ remoterf_server-1.1.0.dev40.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
98
+ remoterf_server-1.1.0.dev40.dist-info/RECORD,,