RemoteRF 2.1.1__tar.gz → 2.1.9__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.
- {remoterf-2.1.1/src/remoterf.egg-info → remoterf-2.1.9}/PKG-INFO +1 -1
- {remoterf-2.1.1 → remoterf-2.1.9}/pyproject.toml +1 -1
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/app.py +35 -18
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/direct_path.py +1 -1
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/grpc_acc.py +43 -30
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/backend.py +2 -2
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/http.py +1 -1
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/remoterf_cli.py +9 -5
- {remoterf-2.1.1 → remoterf-2.1.9/src/remoterf.egg-info}/PKG-INFO +1 -1
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoterf.egg-info/SOURCES.txt +1 -0
- remoterf-2.1.9/tests/test_cli_login_fresh.py +54 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_homes.py +6 -4
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_remoterf_cli.py +1 -24
- {remoterf-2.1.1 → remoterf-2.1.9}/LICENSE +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/README.md +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/setup.cfg +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc.proto +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/banner.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/grpc_client.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/secure_channel.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/direct.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/homes.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/deployment/state.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/dynamic_device.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/dynamic_v2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/version.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_banner.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_config_address.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_direct_path_race.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_federated_devices.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_fetch_idl_fallback.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_finish_prompt.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_native_devices.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_process_arg.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.1.1 → remoterf-2.1.9}/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.
|
|
3
|
+
Version: 2.1.9
|
|
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.
|
|
7
|
+
version = "2.1.9"
|
|
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("
|
|
234
|
-
account.backend.verify(_ask('Verification
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,
|
|
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={"
|
|
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=
|
|
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
|
-
|
|
341
|
-
|
|
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
|
|
382
|
-
#
|
|
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.
|
|
3
|
+
Version: 2.1.9
|
|
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
|
|
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) ==
|
|
322
|
-
used_global.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf-2.1.1 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|