RemoteRF 2.1.1__tar.gz → 2.1.10__tar.gz

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.
Files changed (95) hide show
  1. {remoterf-2.1.1/src/remoterf.egg-info → remoterf-2.1.10}/PKG-INFO +1 -1
  2. {remoterf-2.1.1 → remoterf-2.1.10}/pyproject.toml +1 -1
  3. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/app.py +35 -18
  4. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/direct_path.py +1 -1
  5. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/dynamic_v2_transport.py +2 -1
  6. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/grpc_acc.py +43 -30
  7. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/backend.py +2 -2
  8. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/http.py +1 -1
  9. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/remoterf_cli.py +9 -5
  10. {remoterf-2.1.1 → remoterf-2.1.10/src/remoterf.egg-info}/PKG-INFO +1 -1
  11. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoterf.egg-info/SOURCES.txt +1 -0
  12. remoterf-2.1.10/tests/test_cli_login_fresh.py +54 -0
  13. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_homes.py +6 -4
  14. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_remoterf_cli.py +1 -24
  15. {remoterf-2.1.1 → remoterf-2.1.10}/LICENSE +0 -0
  16. {remoterf-2.1.1 → remoterf-2.1.10}/README.md +0 -0
  17. {remoterf-2.1.1 → remoterf-2.1.10}/setup.cfg +0 -0
  18. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/__init__.py +0 -0
  19. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/__init__.py +0 -0
  20. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/__init__.py +0 -0
  21. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/deployment_capabilities.proto +0 -0
  22. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +0 -0
  23. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  24. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/grpc.proto +0 -0
  25. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
  26. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
  27. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/grpc/v2_codec.py +0 -0
  28. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/tos_notice.txt +0 -0
  29. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/__init__.py +0 -0
  30. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/ansi_codes.py +0 -0
  31. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/api_token.py +0 -0
  32. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/banner.py +0 -0
  33. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/list_string.py +0 -0
  34. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/common/utils/process_arg.py +0 -0
  35. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/config/__init__.py +0 -0
  36. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/config/cert_fetcher.py +0 -0
  37. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/config/config.py +0 -0
  38. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/__init__.py +0 -0
  39. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/acc_login.py +0 -0
  40. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/banner_note.txt +0 -0
  41. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/grpc_client.py +0 -0
  42. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/secure_channel.py +0 -0
  43. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/core/v2_errors.py +0 -0
  44. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/__init__.py +0 -0
  45. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/direct.py +0 -0
  46. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/homes.py +0 -0
  47. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/deployment/state.py +0 -0
  48. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/__init__.py +0 -0
  49. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/_virtual.py +0 -0
  50. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
  51. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
  52. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/dynamic_device.py +0 -0
  53. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/dynamic_v2.py +0 -0
  54. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
  55. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
  56. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
  57. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  58. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/sim_sdr/__init__.py +0 -0
  59. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +0 -0
  60. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/support/__init__.py +0 -0
  61. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
  62. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/support/uhd.py +0 -0
  63. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
  64. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
  65. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
  66. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  67. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/usrp/__init__.py +0 -0
  68. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
  69. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
  70. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF/version.py +0 -0
  71. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoteRF.py +0 -0
  72. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoterf.egg-info/dependency_links.txt +0 -0
  73. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoterf.egg-info/entry_points.txt +0 -0
  74. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoterf.egg-info/requires.txt +0 -0
  75. {remoterf-2.1.1 → remoterf-2.1.10}/src/remoterf.egg-info/top_level.txt +0 -0
  76. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_banner.py +0 -0
  77. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_config_address.py +0 -0
  78. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_deployment_accounts.py +0 -0
  79. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_direct_path_fixed.py +0 -0
  80. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_direct_path_partner.py +0 -0
  81. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_direct_path_race.py +0 -0
  82. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_dynamic_device_codegen.py +0 -0
  83. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_dynamic_v2.py +0 -0
  84. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_federated_devices.py +0 -0
  85. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_fetch_idl_fallback.py +0 -0
  86. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_finish_prompt.py +0 -0
  87. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_gate_i_direct_path.py +0 -0
  88. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_grpc_client_target.py +0 -0
  89. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_native_account_credentials.py +0 -0
  90. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_native_devices.py +0 -0
  91. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_partner_dynamic_v2.py +0 -0
  92. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_pluto_virtual.py +0 -0
  93. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_process_arg.py +0 -0
  94. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_ti_mmwave.py +0 -0
  95. {remoterf-2.1.1 → remoterf-2.1.10}/tests/test_upgrade_rescue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.1.1
3
+ Version: 2.1.10
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoterf"
7
- version = "2.1.1"
7
+ version = "2.1.10"
8
8
  description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -230,8 +230,8 @@ def _account_command(command):
230
230
  # out of quota); there is no token coming. Straight on to login.
231
231
  return False
232
232
  printf("Verify your email", (Sty.BOLD, Sty.BLUE))
233
- printf(" Paste the token from the verification email.", Sty.GRAY)
234
- account.backend.verify(_ask('Verification token', hidden=True))
233
+ printf(" Enter the 6-digit code from the verification email.", Sty.GRAY)
234
+ account.backend.verify(account.username or account.email, _ask('Verification code').strip())
235
235
  printf('Email verified. You can now log in.', (Sty.BOLD, Sty.GREEN))
236
236
  return False
237
237
  if command in ('forgot-password', 'reset-password'):
@@ -349,7 +349,8 @@ def commands():
349
349
  printf("'cancelres' ", Sty.MAGENTA, " : ", Sty.GRAY, "Cancel a reservation", Sty.DEFAULT)
350
350
  printf("'getres' ", Sty.MAGENTA, " : ", Sty.GRAY, "View all reservations", Sty.DEFAULT)
351
351
  printf("'myres' ", Sty.MAGENTA, " : ", Sty.GRAY, "View my reservations", Sty.DEFAULT)
352
- printf("'perms' ", Sty.MAGENTA, " : ", Sty.GRAY, "View permissions", Sty.DEFAULT)
352
+ if not account.is_https_home:
353
+ printf("'perms' ", Sty.MAGENTA, " : ", Sty.GRAY, "View permissions", Sty.DEFAULT)
353
354
  printf("'enroll' ", Sty.MAGENTA, " : ", Sty.GRAY, "Enroll with an enrollment code", Sty.DEFAULT)
354
355
  printf("'logout' ", Sty.MAGENTA, " : ", Sty.GRAY, "Log out and forget the stored login", Sty.DEFAULT)
355
356
  printf("'status' ", Sty.MAGENTA, " : ", Sty.GRAY, "Connection and the path device calls use", Sty.DEFAULT)
@@ -538,6 +539,8 @@ def _federated_reserve_slot(device_id, chosen_day, chosen_slot, slot_start_str,
538
539
  printf("Reservation successful for ", (Sty.BOLD, Sty.GREEN), f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
539
540
  printf("Your device handle -> ", Sty.BOLD, f"{handle}", (Sty.BOLD, Sty.GREEN))
540
541
  printf("Use it where a token goes, e.g. adi.Pluto(\"" + handle + "\"); it works while you are logged in to this home.", Sty.DEFAULT)
542
+ printf("The first call can take ~20 s while the radio powers on.", Sty.GRAY)
543
+ account.install_reserved_driver(handle, device_id)
541
544
 
542
545
 
543
546
  def _federated_reserve():
@@ -581,7 +584,8 @@ def _federated_reserve():
581
584
  )
582
585
  return
583
586
  _pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, _federated_reserve_slot,
584
- device_label=f"{dev['display_name']} @{dev['deployment_name']}")
587
+ device_label=f"{dev['display_name']} @{dev['deployment_name']}",
588
+ max_days=-(-int(dev.get("max_horizon_sec") or 0) // 86400) or None)
585
589
 
586
590
 
587
591
  def reservations():
@@ -841,6 +845,7 @@ def reserve():
841
845
  if token != '':
842
846
  printf(f"Reservation successful. Your Token -> ", Sty.BOLD, f"{token}", Sty.BG_GREEN)
843
847
  printf(f"Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one. ", Sty.DEFAULT)
848
+ account.install_reserved_driver(token, rows[int(pick) - 1]["id"])
844
849
  except Exception as e:
845
850
  printf(f"Error: {e}", Sty.BRIGHT_RED)
846
851
 
@@ -985,7 +990,7 @@ def perms():
985
990
  printf("Accessible Devices: ", (Sty.BOLD, Sty.BLUE), f"{devices}", Sty.MAGENTA)
986
991
 
987
992
  # Build per-device caps and group identical limits together
988
- buckets: dict[tuple[int, int], list[int]] = {} # (max_r, max_t_sec) -> [dev_ids]
993
+ buckets: dict[tuple[int, int, int], list[int]] = {} # (max_r, max_t_sec, horizon_sec) -> [dev_ids]
989
994
  for d in devices:
990
995
  try:
991
996
  did = int(d)
@@ -994,7 +999,8 @@ def perms():
994
999
  c = _cap_for(did)
995
1000
  max_t = int(c.get("max_reservation_time_sec", 0) or 0)
996
1001
  max_r = int(c.get("max_reservations", 0) or 0)
997
- buckets.setdefault((max_r, max_t), []).append(did)
1002
+ max_h = int(c.get("max_horizon_sec", 0) or 0)
1003
+ buckets.setdefault((max_r, max_t, max_h), []).append(did)
998
1004
 
999
1005
  if not buckets:
1000
1006
  print("Limits per device: (none)")
@@ -1003,16 +1009,17 @@ def perms():
1003
1009
 
1004
1010
  # If everything shares the same limits, print once
1005
1011
  if len(buckets) == 1:
1006
- (max_r, max_t), _devs = next(iter(buckets.items()))
1012
+ (max_r, max_t, max_h), _devs = next(iter(buckets.items()))
1007
1013
  printf("Permissions:", (Sty.BOLD, Sty.BLUE))
1008
1014
  printf(" Max Reservations: ", Sty.GRAY, f"{max_r}", Sty.CYAN)
1009
1015
  printf(" Reservation Duration (min): ", Sty.GRAY, f"{max_t // 60}", Sty.CYAN)
1016
+ printf(" Days Ahead: ", Sty.GRAY, f"{max_h / 86400:g}" if max_h else "no limit", Sty.CYAN)
1010
1017
  render_remote_permissions()
1011
1018
  return
1012
1019
 
1013
1020
  # Otherwise print grouped limits
1014
1021
  printf("Permissions per device (grouped):", (Sty.BOLD, Sty.BLUE))
1015
- for (max_r, max_t), devs in sorted(buckets.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[1])):
1022
+ for (max_r, max_t, max_h), devs in sorted(buckets.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[0][2], kv[1])):
1016
1023
  devs = sorted(devs)
1017
1024
 
1018
1025
  # compress ranges like 0-3,5,7-9
@@ -1031,7 +1038,7 @@ def perms():
1031
1038
  ranges.append(f"{start}-{prev}" if start != prev else f"{start}")
1032
1039
 
1033
1040
  dev_str = ",".join(ranges)
1034
- printf(" devices[", Sty.GRAY, f"{dev_str}", Sty.MAGENTA, "]: ", Sty.GRAY, f"max_reservations={max_r}, max_time_min={max_t // 60}", Sty.CYAN)
1041
+ printf(" devices[", Sty.GRAY, f"{dev_str}", Sty.MAGENTA, "]: ", Sty.GRAY, f"max_reservations={max_r}, max_time_min={max_t // 60}, days_ahead={f'{max_h / 86400:g}' if max_h else 'no limit'}", Sty.CYAN)
1035
1042
 
1036
1043
  render_remote_permissions()
1037
1044
 
@@ -1278,6 +1285,7 @@ def interactive_reserve_next_days(block_minutes=60):
1278
1285
  printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{chosen_device_id}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
1279
1286
  printf("Your Token -> ", Sty.BOLD, f"{token}", (Sty.BOLD, Sty.GREEN))
1280
1287
  printf("Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one.", Sty.DEFAULT)
1288
+ account.install_reserved_driver(token, int(chosen_device_id))
1281
1289
 
1282
1290
  except Exception as e:
1283
1291
  print(f"Error: {e}")
@@ -1306,16 +1314,18 @@ def _auto_block_minutes(max_t_sec: int) -> int:
1306
1314
  break
1307
1315
  return best
1308
1316
 
1309
- def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, reservations_for_range, do_reserve, *, device_label=None):
1317
+ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, reservations_for_range, do_reserve, *, device_label=None, max_days=None):
1310
1318
  """Shared `resdev` slot picker: days -> free slots -> pick -> confirm -> reserve.
1311
1319
  Native and federated homes differ only in how reservations are fetched and
1312
- how the reservation is made (token vs. HOME handle)."""
1320
+ how the reservation is made (token vs. HOME handle). `max_days` is the
1321
+ group's reservation horizon; the server refuses starts beyond it anyway."""
1313
1322
  device_label = chosen_device_id if device_label is None else device_label
1314
- num_days_s = session.prompt(stylize("Enter the number of days to check for available reservations (starting today): ", (Sty.BOLD, Sty.GREEN))).strip()
1323
+ limit = f" (up to {max_days})" if max_days else ""
1324
+ num_days_s = session.prompt(stylize(f"Enter the number of days to check for available reservations (starting today){limit}: ", (Sty.BOLD, Sty.GREEN))).strip()
1315
1325
  try:
1316
1326
  num_days = int(num_days_s)
1317
- if num_days <= 0:
1318
- print("Invalid number of days.")
1327
+ if num_days <= 0 or (max_days and num_days > max_days):
1328
+ print("Invalid number of days." if num_days <= 0 else f"Your group may reserve at most {max_days} days ahead.")
1319
1329
  return
1320
1330
  except ValueError:
1321
1331
  print("Invalid input. Please enter a number.")
@@ -1419,6 +1429,7 @@ def _native_reserve_slot(chosen_device_id, chosen_day, chosen_slot, slot_start_s
1419
1429
  printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{chosen_device_id}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
1420
1430
  printf("Your Token -> ", Sty.BOLD, f"{token}", (Sty.BOLD, Sty.GREEN))
1421
1431
  printf("Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one.", Sty.DEFAULT)
1432
+ account.install_reserved_driver(token, int(chosen_device_id))
1422
1433
 
1423
1434
 
1424
1435
  def interactive_reserve_next_days_auto():
@@ -1492,7 +1503,10 @@ def interactive_reserve_next_days_auto():
1492
1503
  sorted_device_ids = sorted(dev_resp.results.keys(), key=int) # strings
1493
1504
 
1494
1505
  if allowed_dev_ids is not None:
1495
- sorted_device_ids = [d for d in sorted_device_ids if int(d) in allowed_dev_ids]
1506
+ # A partner lab's device is listed only when this account is enrolled
1507
+ # there; it needs no local group (and has no local caps: 30 min blocks).
1508
+ sorted_device_ids = [d for d in sorted_device_ids
1509
+ if int(d) in allowed_dev_ids or _PARTNER_LABEL.match(str(unmap_arg(dev_resp.results[d])))]
1496
1510
 
1497
1511
  if not sorted_device_ids:
1498
1512
  printf("No devices available for your permission level.", Sty.WARNING)
@@ -1501,6 +1515,7 @@ def interactive_reserve_next_days_auto():
1501
1515
  printf("Devices:", Sty.BOLD)
1502
1516
  block_by_dev: dict[str, int] = {}
1503
1517
  max_by_dev: dict[str, int] = {}
1518
+ days_by_dev: dict[str, int] = {}
1504
1519
 
1505
1520
  for idx, dev_id in enumerate(sorted_device_ids):
1506
1521
  dev_name = unmap_arg(dev_resp.results[dev_id])
@@ -1510,6 +1525,7 @@ def interactive_reserve_next_days_auto():
1510
1525
  c = _cap_for(int(dev_id))
1511
1526
  try:
1512
1527
  max_t_sec = int(c.get("max_reservation_time_sec", 0) or 0)
1528
+ days_by_dev[str(dev_id)] = -(-int(c.get("max_horizon_sec", 0) or 0) // 86400)
1513
1529
  except Exception:
1514
1530
  max_t_sec = 0
1515
1531
  elif perm_level == "Power User" and power_user_max_t_sec is not None:
@@ -1562,7 +1578,8 @@ def interactive_reserve_next_days_auto():
1562
1578
  )
1563
1579
  return
1564
1580
 
1565
- _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, fetch_reservations_for_range, _native_reserve_slot)
1581
+ _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, fetch_reservations_for_range, _native_reserve_slot,
1582
+ max_days=days_by_dev.get(chosen_device_id) or None)
1566
1583
 
1567
1584
  except Exception as e:
1568
1585
  print(f"Error: {e}")
@@ -1635,7 +1652,7 @@ def run(backend=None, *, register=False, enrollment_code=None, server_label=None
1635
1652
  devices()
1636
1653
  elif inpu == "help" or inpu == "h":
1637
1654
  commands()
1638
- elif inpu == "perms":
1655
+ elif inpu == "perms" and not account.is_https_home:
1639
1656
  perms()
1640
1657
  elif inpu == "enroll":
1641
1658
  enroll()
@@ -1666,7 +1683,7 @@ def run(backend=None, *, register=False, enrollment_code=None, server_label=None
1666
1683
  reserve()
1667
1684
  elif account.is_admin and inpu.strip().startswith("admin"):
1668
1685
  handle_admin_command(inpu)
1669
- else:
1686
+ elif inpu.strip():
1670
1687
  print(f"Unknown command: {inpu}")
1671
1688
  except AccountBackendError as exc:
1672
1689
  label = f" [{exc.provenance}]" if exc.provenance else ""
@@ -124,7 +124,7 @@ def quiet_aioice_errors(loop) -> None:
124
124
  where = repr(context.get("handle", "")) + str(context.get("message", ""))
125
125
  if isinstance(exc, (AttributeError, OSError)) and "Transaction.__retry" in where:
126
126
  return
127
- if isinstance(exc, ConnectionError) and "exception was never retrieved" in where:
127
+ if isinstance(exc, ConnectionError) and ("exception was never retrieved" in where or "shielded future" in where):
128
128
  return
129
129
  default(context)
130
130
 
@@ -157,7 +157,8 @@ class DynamicV2Transport:
157
157
  self._stream_poisoned = False
158
158
  self._stream_failure_details = {}
159
159
  if control_stub is None or sample_stub is None:
160
- from .grpc_client import channel
160
+ from .grpc_client import get_active_channel
161
+ channel = get_active_channel()
161
162
  control_stub = control_stub or grpc_pb2_grpc.DynamicControlV2Stub(channel)
162
163
  sample_stub = sample_stub or grpc_pb2_grpc.SampleDataV1Stub(channel)
163
164
  self.control = control_stub
@@ -20,6 +20,12 @@ from ..common.utils import *
20
20
 
21
21
  import datetime
22
22
 
23
+ def _busy(label):
24
+ """A spinner while the home asks its labs: a slow lab is a wait, not a hang."""
25
+ from ..remoterf_cli import _working
26
+ return _working(label)
27
+
28
+
23
29
  class RemoteRFAccount:
24
30
  def __init__(self, username:str=None, password:str=None, email:str=None, *, backend=None):
25
31
  self.username = username
@@ -93,51 +99,58 @@ class RemoteRFAccount:
93
99
 
94
100
  def reserve_device(self, device_id, start_time:datetime, end_time:datetime):
95
101
  if self.is_https_home:
96
- reservation = self.backend.reserve(device_id, int(start_time.timestamp()), int(end_time.timestamp()))
97
- try:
98
- from ..drivers.dynamic_device import install_driver
99
- install_driver(device_id=reservation['device_id'])
100
- except Exception as e:
101
- print(f"Warning: could not install driver for device {reservation['device_id']}: {e}")
102
+ with _busy("Reserving…"):
103
+ reservation = self.backend.reserve(device_id, int(start_time.timestamp()), int(end_time.timestamp()))
102
104
  # The HOME reference is the driver handle: adi.Pluto("<deployment>:<id>")
103
105
  return reservation['device_id']
104
106
  username, credential_secret = self._rpc_credentials()
105
- response = self._call(function_name="ACC:reserve_device", args={"un":map_arg(username), "pw":map_arg(credential_secret), "dd":map_arg(device_id), "st":map_arg(int(start_time.timestamp())), "et":map_arg(int(end_time.timestamp()))})
107
+ with _busy("Reserving…"):
108
+ response = self._call(function_name="ACC:reserve_device", args={"un":map_arg(username), "pw":map_arg(credential_secret), "dd":map_arg(device_id), "st":map_arg(int(start_time.timestamp())), "et":map_arg(int(end_time.timestamp()))})
106
109
 
107
110
  if 'ace' in response.results:
108
111
  raise Exception(f'{unmap_arg(response.results["ace"])}')
109
112
  elif 'Token' in response.results:
110
- token = unmap_arg(response.results["Token"])
111
- try:
112
- from ..drivers.dynamic_device import install_driver
113
- install_driver(token=token, device_id=device_id)
114
- except Exception as e:
115
- print(f"Warning: could not install driver for device {device_id}: {e}")
116
- return token
113
+ return unmap_arg(response.results["Token"])
114
+
115
+ def install_reserved_driver(self, handle, device_id):
116
+ """Separate from reserve_device: the token is shown before this network
117
+ step runs, so a failed or stuck driver fetch cannot lose it."""
118
+ try:
119
+ from ..drivers.dynamic_device import install_driver
120
+ if self.is_https_home:
121
+ install_driver(device_id=handle)
122
+ else:
123
+ install_driver(token=handle, device_id=device_id)
124
+ except Exception as e:
125
+ print(f"Warning: could not install driver for device {device_id}: {e}")
117
126
 
118
127
  def get_reservations(self):
119
- if self.is_https_home:
120
- return self.backend.reservations()
121
- username, credential_secret = self._rpc_credentials()
122
- return self._call(function_name='ACC:get_res', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
128
+ with _busy("Asking the labs for reservations…"):
129
+ if self.is_https_home:
130
+ return self.backend.reservations()
131
+ username, credential_secret = self._rpc_credentials()
132
+ return self._call(function_name='ACC:get_res', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
123
133
 
124
134
  def get_devices(self):
125
- if self.is_https_home:
126
- return self.backend.devices()
127
- username, credential_secret = self._rpc_credentials()
128
- return self._call(function_name='ACC:get_dev', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
135
+ with _busy("Asking the labs for devices…"):
136
+ if self.is_https_home:
137
+ return self.backend.devices()
138
+ username, credential_secret = self._rpc_credentials()
139
+ return self._call(function_name='ACC:get_dev', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
129
140
 
130
141
  def cancel_reservation(self, res_id):
131
- if self.is_https_home:
132
- return self.backend.cancel(res_id)
133
- username, credential_secret = self._rpc_credentials()
134
- return self._call(function_name='ACC:cancel_res', args={"un":map_arg(username), "pw":map_arg(credential_secret), "res_id":map_arg(res_id)})
142
+ with _busy("Cancelling…"):
143
+ if self.is_https_home:
144
+ return self.backend.cancel(res_id)
145
+ username, credential_secret = self._rpc_credentials()
146
+ return self._call(function_name='ACC:cancel_res', args={"un":map_arg(username), "pw":map_arg(credential_secret), "res_id":map_arg(res_id)})
135
147
 
136
148
  def get_perms(self):
137
- if self.is_https_home:
138
- return self.backend.permissions()
139
- username, credential_secret = self._rpc_credentials()
140
- return self._call(function_name='ACC:get_perms', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
149
+ with _busy("Asking the labs for permissions…"):
150
+ if self.is_https_home:
151
+ return self.backend.permissions()
152
+ username, credential_secret = self._rpc_credentials()
153
+ return self._call(function_name='ACC:get_perms', args={"un":map_arg(username), "pw":map_arg(credential_secret)})
141
154
 
142
155
  def logout(self):
143
156
  """Revoke the stored session on the deployment (best effort)."""
@@ -111,10 +111,10 @@ class HttpsJsonAccountBackend(DeploymentAccountBackend):
111
111
  data={"username": name, "email": email, "password": password},
112
112
  )
113
113
 
114
- def verify(self, token):
114
+ def verify(self, login, code):
115
115
  self._operation("ACC:verify_email")
116
116
  return self.transport.request(
117
- "POST", self.base + "/verify-email", data={"token": token}
117
+ "POST", self.base + "/verify-email", data={"login": login, "code": code}
118
118
  )
119
119
 
120
120
  def _validate_session(self, value, expected_subject=None):
@@ -73,7 +73,7 @@ class JsonTransport:
73
73
  timeout=timeout,
74
74
  ) as response:
75
75
  raw = bytearray()
76
- for chunk in response.iter_bytes(chunk_size=1):
76
+ for chunk in response.iter_bytes(chunk_size=65536):
77
77
  if time.monotonic() >= deadline:
78
78
  raise AccountBackendError("Account operation deadline exceeded")
79
79
  raw.extend(chunk)
@@ -332,13 +332,15 @@ def _register() -> int:
332
332
  return 1
333
333
  remember_tos_agreement()
334
334
  try:
335
- entered = input('Enrollment code: ').strip()
335
+ entered = input('Enrollment code (Enter for a RemoteRF Global account): ').strip()
336
336
  except (EOFError, KeyboardInterrupt):
337
337
  print()
338
338
  return 1
339
339
  if not entered:
340
- printf('An enrollment code is required. Ask your lab for one.', Sty.WARNING)
341
- return 2
340
+ # No lab yet: the account lives on Global. A lab's invite can still
341
+ # be redeemed later, from the shell, with `enroll <invite>`.
342
+ printf('Registering a RemoteRF Global account. Join a lab any time with: enroll <lab invite>', Sty.GRAY)
343
+ return _use_global(register=True, show_banner=False)
342
344
  try:
343
345
  host, code = homes.parse_code(entered)
344
346
  except ValidationError as exc:
@@ -378,8 +380,10 @@ def _login(target: str | None) -> int:
378
380
  elif saved:
379
381
  return _print_pick_a_target()
380
382
  else:
381
- # Nothing has been saved: fall through to whatever the client was
382
- # configured with directly, exactly as before homes existed.
383
+ # Nothing saved. A legacy address in .env still logs in there;
384
+ # a fresh install has nothing to log in to, so `-l` is `-r`.
385
+ if not _ensure_config_present()[0]:
386
+ return _register()
383
387
  return _account_shell(register=False)
384
388
  if target == 'global':
385
389
  if not homes.has_global_account():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.1.1
3
+ Version: 2.1.10
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -69,6 +69,7 @@ src/remoterf.egg-info/entry_points.txt
69
69
  src/remoterf.egg-info/requires.txt
70
70
  src/remoterf.egg-info/top_level.txt
71
71
  tests/test_banner.py
72
+ tests/test_cli_login_fresh.py
72
73
  tests/test_config_address.py
73
74
  tests/test_deployment_accounts.py
74
75
  tests/test_direct_path_fixed.py
@@ -0,0 +1,54 @@
1
+ """`remoterf -l` on a fresh install is `remoterf -r`; a configured client logs in."""
2
+
3
+ import os
4
+ import tempfile
5
+ from unittest import mock
6
+
7
+
8
+ def _fresh_home():
9
+ tmp = tempfile.TemporaryDirectory()
10
+ env = {k: v for k, v in os.environ.items() if not k.startswith("REMOTERF_")}
11
+ env["HOME"] = tmp.name
12
+ return tmp, mock.patch.dict(os.environ, env, clear=True)
13
+
14
+
15
+ def test_login_on_a_fresh_install_registers():
16
+ from remoteRF import remoterf_cli
17
+
18
+ tmp, env = _fresh_home()
19
+ with tmp, env, \
20
+ mock.patch("remoteRF.config.config.require_tos_for_login", return_value=True), \
21
+ mock.patch.object(remoterf_cli, "_register", return_value=0) as register, \
22
+ mock.patch.object(remoterf_cli, "_account_shell") as shell:
23
+ assert remoterf_cli._login(None) == 0
24
+ register.assert_called_once_with()
25
+ shell.assert_not_called()
26
+
27
+
28
+ def test_login_with_a_legacy_address_still_logs_in_there():
29
+ from remoteRF import remoterf_cli
30
+
31
+ tmp, env = _fresh_home()
32
+ with tmp, env, \
33
+ mock.patch("remoteRF.config.config.require_tos_for_login", return_value=True), \
34
+ mock.patch.object(remoterf_cli, "_ensure_config_present", return_value=(True, "")), \
35
+ mock.patch.object(remoterf_cli, "_register") as register, \
36
+ mock.patch.object(remoterf_cli, "_account_shell", return_value=0) as shell:
37
+ assert remoterf_cli._login(None) == 0
38
+ shell.assert_called_once_with(register=False)
39
+ register.assert_not_called()
40
+
41
+
42
+ def test_register_without_a_code_makes_a_global_account(monkeypatch):
43
+ from remoteRF import remoterf_cli
44
+
45
+ monkeypatch.setattr("builtins.input", lambda prompt="": "")
46
+ with mock.patch("remoteRF.config.config._confirm_tos", return_value=True), \
47
+ mock.patch("remoteRF.config.config.forget_tos_agreement"), \
48
+ mock.patch("remoteRF.config.config.remember_tos_agreement"), \
49
+ mock.patch.object(remoterf_cli, "print_client_banner"), \
50
+ mock.patch.object(remoterf_cli, "_use_global", return_value=0) as use_global, \
51
+ mock.patch.object(remoterf_cli, "_account_shell") as shell:
52
+ assert remoterf_cli._register() == 0
53
+ use_global.assert_called_once_with(register=True, show_banner=False)
54
+ shell.assert_not_called()
@@ -314,12 +314,13 @@ def test_register_with_a_code_lands_in_the_home_shell(monkeypatch, reachable):
314
314
  assert homes.last_target() == {"kind": "home", "name": "ucla"}
315
315
 
316
316
 
317
- def test_a_blank_code_is_refused_and_global_has_its_own_spelling(monkeypatch):
317
+ def test_a_blank_code_registers_on_global_and_global_has_its_own_spelling(monkeypatch):
318
318
  answers = iter(["y", ""])
319
319
  monkeypatch.setattr("builtins.input", lambda *_: next(answers, ""))
320
320
  used_global = mock.Mock(return_value=0)
321
- assert run_cli(["-r"], monkeypatch, _use_global=used_global) == 2
322
- used_global.assert_not_called()
321
+ assert run_cli(["-r"], monkeypatch, _use_global=used_global) == 0
322
+ used_global.assert_called_once_with(register=True, show_banner=False) # no lab: the account lives on Global
323
+ used_global.reset_mock()
323
324
  assert run_cli(["-r", "global"], monkeypatch, _use_global=used_global) == 0
324
325
  used_global.assert_called_once_with(register=True)
325
326
 
@@ -337,7 +338,8 @@ def test_declining_the_terms_stops_registration_before_any_code(monkeypatch):
337
338
  answers = iter(["y"])
338
339
  monkeypatch.setattr("builtins.input", lambda prompt="": next(answers))
339
340
  shell = mock.Mock(return_value=0)
340
- assert run_cli(["-l"], monkeypatch, _account_shell=shell) == 0
341
+ # A configured client (a legacy address) logs in; unconfigured, `-l` would register.
342
+ assert run_cli(["-l"], monkeypatch, _account_shell=shell, _ensure_config_present=lambda: (True, "")) == 0
341
343
  assert cfg.tos_agreed()
342
344
  monkeypatch.setattr("builtins.input", lambda prompt="": (_ for _ in ()).throw(AssertionError(prompt)))
343
345
  assert run_cli(["-l"], monkeypatch, _account_shell=shell) == 0 # remembered: no prompt
@@ -5,30 +5,7 @@ from remoteRF import remoterf_cli
5
5
 
6
6
 
7
7
  class RemoteRFCliTests(unittest.TestCase):
8
- @mock.patch.object(remoterf_cli, "printf")
9
- @mock.patch.object(remoterf_cli, "print_client_banner")
10
- @mock.patch.object(
11
- remoterf_cli,
12
- "_ensure_config_present",
13
- return_value=(False, "not configured"),
14
- )
15
- @mock.patch.object(remoterf_cli, "_installed_version", return_value="2.0.9")
16
- @mock.patch.object(remoterf_cli.sys, "argv", ["remoterf", "-l"])
17
- def test_unconfigured_login_shows_na_banner_and_configure_message(
18
- self,
19
- _version,
20
- _ensure_config,
21
- print_banner,
22
- printf,
23
- ):
24
- self.assertEqual(remoterf_cli.main(), 2)
25
- print_banner.assert_called_once_with("2.0.9", server="")
26
- self.assertTrue(
27
- any(
28
- call.args and call.args[0] == "Please configure remoterf properly first."
29
- for call in printf.call_args_list
30
- )
31
- )
8
+ # `-l` on an unconfigured client registers instead: tests/test_cli_login_fresh.py
32
9
 
33
10
  @mock.patch.object(remoterf_cli, "_print_server_unavailable")
34
11
  @mock.patch.object(remoterf_cli, "_connected_server", return_value=None)
File without changes
File without changes
File without changes
File without changes
File without changes