RemoteRF 2.1.0__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.0 → remoterf-2.1.9}/PKG-INFO +1 -1
- {remoterf-2.1.0 → remoterf-2.1.9}/pyproject.toml +1 -1
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/app.py +35 -18
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/direct_path.py +105 -40
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/grpc_acc.py +43 -30
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/grpc_client.py +6 -8
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/backend.py +2 -2
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/http.py +1 -1
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/remoterf_cli.py +11 -7
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/PKG-INFO +1 -1
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/SOURCES.txt +2 -1
- remoterf-2.1.9/tests/test_cli_login_fresh.py +54 -0
- remoterf-2.1.9/tests/test_direct_path_race.py +129 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_finish_prompt.py +8 -3
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_gate_i_direct_path.py +10 -4
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_homes.py +6 -4
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_remoterf_cli.py +1 -24
- remoterf-2.1.0/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -291
- {remoterf-2.1.0 → remoterf-2.1.9}/LICENSE +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/README.md +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/setup.cfg +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc.proto +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/banner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/secure_channel.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/direct.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/homes.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/deployment/state.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/dynamic_device.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF/version.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoteRF.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_banner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_config_address.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_federated_devices.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_fetch_idl_fallback.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_native_devices.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_process_arg.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.9}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.1.0 → 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 ""
|
|
@@ -32,7 +32,8 @@ path; everything else still takes the home's.
|
|
|
32
32
|
When the answer names a *fixed port* (a Server with a reachable address
|
|
33
33
|
listening for QUIC on one admitted UDP port), the path dials it like any QUIC
|
|
34
34
|
server -- no punch, so it works behind any NAT -- and claims the session with
|
|
35
|
-
the secret the answer carried.
|
|
35
|
+
the secret the answer carried. The dial and the punch run at once; the first
|
|
36
|
+
to connect wins.
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
39
|
from __future__ import annotations
|
|
@@ -112,7 +113,10 @@ def quiet_aioice_errors(loop) -> None:
|
|
|
112
113
|
|
|
113
114
|
The retry fires from a TimerHandle named for aioice's Transaction and
|
|
114
115
|
dies inside asyncio's own transport code once the socket is gone (an
|
|
115
|
-
AttributeError on the closed transport or its dropped loop).
|
|
116
|
+
AttributeError on the closed transport or its dropped loop).
|
|
117
|
+
|
|
118
|
+
Likewise aioquic's shielded connect waiter: a handshake that ran out of
|
|
119
|
+
time leaves it to fail with a ConnectionError nobody reads."""
|
|
116
120
|
default = loop.default_exception_handler
|
|
117
121
|
|
|
118
122
|
def handler(loop, context):
|
|
@@ -120,6 +124,8 @@ def quiet_aioice_errors(loop) -> None:
|
|
|
120
124
|
where = repr(context.get("handle", "")) + str(context.get("message", ""))
|
|
121
125
|
if isinstance(exc, (AttributeError, OSError)) and "Transaction.__retry" in where:
|
|
122
126
|
return
|
|
127
|
+
if isinstance(exc, ConnectionError) and ("exception was never retrieved" in where or "shielded future" in where):
|
|
128
|
+
return
|
|
123
129
|
default(context)
|
|
124
130
|
|
|
125
131
|
loop.set_exception_handler(handler)
|
|
@@ -189,6 +195,10 @@ class PathError(Exception):
|
|
|
189
195
|
"""The direct path could not carry this call and is down now."""
|
|
190
196
|
|
|
191
197
|
|
|
198
|
+
def _why(exc: BaseException) -> str:
|
|
199
|
+
return "timed out" if isinstance(exc, asyncio.TimeoutError) else f"{type(exc).__name__}: {exc}"
|
|
200
|
+
|
|
201
|
+
|
|
192
202
|
class _IceTransport:
|
|
193
203
|
"""The sendto() surface aioquic's protocol needs, on a nominated ICE pair."""
|
|
194
204
|
|
|
@@ -354,10 +364,7 @@ class DirectPath:
|
|
|
354
364
|
try:
|
|
355
365
|
await asyncio.wait_for(self._establish(), SETTLE_SECONDS)
|
|
356
366
|
except Exception as exc: # noqa: BLE001 - a failed punch is expected on some networks
|
|
357
|
-
self.reason = (
|
|
358
|
-
"timed out" if isinstance(exc, asyncio.TimeoutError)
|
|
359
|
-
else f"{type(exc).__name__}: {exc}"
|
|
360
|
-
)
|
|
367
|
+
self.reason = str(exc) if isinstance(exc, PathError) else _why(exc)
|
|
361
368
|
self.state = "down"
|
|
362
369
|
if "No matching server function" in self.reason:
|
|
363
370
|
self.state = "unavailable"
|
|
@@ -397,6 +404,10 @@ class DirectPath:
|
|
|
397
404
|
"ufrag": ice.local_username,
|
|
398
405
|
"pwd": ice.local_password,
|
|
399
406
|
"candidates": [candidate.to_sdp() for candidate in ice.local_candidates],
|
|
407
|
+
# This client punches at once (no fixed-port head start), so the
|
|
408
|
+
# Server may send its first check sooner; a Server that predates
|
|
409
|
+
# the flag ignores it.
|
|
410
|
+
"race": True,
|
|
400
411
|
}
|
|
401
412
|
answer = await loop.run_in_executor(None, self._offer, offer)
|
|
402
413
|
|
|
@@ -409,35 +420,68 @@ class DirectPath:
|
|
|
409
420
|
)
|
|
410
421
|
configuration.load_verify_locations(cadata=ca_pem)
|
|
411
422
|
|
|
423
|
+
# Both at once. The Server starts its checks the moment it answers,
|
|
424
|
+
# and a NAT admits them only once this side has sent its own: a punch
|
|
425
|
+
# held back for the fixed dial (filtered from off campus) misses that
|
|
426
|
+
# window. On campus the fixed port still wins in milliseconds.
|
|
427
|
+
racers: dict[str, asyncio.Task] = {}
|
|
412
428
|
if answer.get("udp") and answer.get("session"):
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
else:
|
|
418
|
-
await close_ice(ice)
|
|
419
|
-
self._ice = None
|
|
420
|
-
return
|
|
421
|
-
ice.remote_username = str(answer["ufrag"])
|
|
422
|
-
ice.remote_password = str(answer["pwd"])
|
|
423
|
-
for sdp in answer["candidates"]:
|
|
424
|
-
await ice.add_remote_candidate(Candidate.from_sdp(str(sdp)))
|
|
425
|
-
await ice.add_remote_candidate(None)
|
|
426
|
-
await asyncio.wait_for(ice.connect(), ICE_SECONDS)
|
|
427
|
-
peer = peer_address(ice)
|
|
428
|
-
protocol = _Protocol(QuicConnection(configuration=configuration))
|
|
429
|
-
self._transport = _IceTransport(ice)
|
|
430
|
-
protocol.connection_made(self._transport)
|
|
431
|
-
self._tasks = [
|
|
432
|
-
asyncio.ensure_future(self._pump(ice, protocol, peer)),
|
|
433
|
-
asyncio.ensure_future(self._watch(protocol)),
|
|
434
|
-
asyncio.ensure_future(self._watch_sleep()),
|
|
435
|
-
]
|
|
436
|
-
protocol.connect(peer)
|
|
429
|
+
racers["fixed port"] = asyncio.ensure_future(self._dial_fixed(answer, configuration))
|
|
430
|
+
racers["punch"] = asyncio.ensure_future(self._punch(ice, answer, configuration))
|
|
431
|
+
failures: dict[asyncio.Task, Exception] = {}
|
|
432
|
+
pending = set(racers.values())
|
|
437
433
|
try:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
434
|
+
while pending:
|
|
435
|
+
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
|
436
|
+
for task in done:
|
|
437
|
+
try:
|
|
438
|
+
task.result()
|
|
439
|
+
except Exception as exc: # noqa: BLE001 - filtered or refused: the other may still win
|
|
440
|
+
failures[task] = exc
|
|
441
|
+
else:
|
|
442
|
+
return
|
|
443
|
+
finally:
|
|
444
|
+
await cancel_tasks(pending)
|
|
445
|
+
if len(failures) == 1:
|
|
446
|
+
raise next(iter(failures.values()))
|
|
447
|
+
raise PathError("; ".join(f"{name}: {_why(failures[task])}" for name, task in racers.items()))
|
|
448
|
+
|
|
449
|
+
async def _punch(self, ice: Connection, answer: dict, configuration: QuicConfiguration) -> None:
|
|
450
|
+
"""QUIC over the pair ICE nominates; the ICE goes with it when this loses."""
|
|
451
|
+
tasks: list[asyncio.Task] = []
|
|
452
|
+
transport = None
|
|
453
|
+
try:
|
|
454
|
+
ice.remote_username = str(answer["ufrag"])
|
|
455
|
+
ice.remote_password = str(answer["pwd"])
|
|
456
|
+
for sdp in answer["candidates"]:
|
|
457
|
+
await ice.add_remote_candidate(Candidate.from_sdp(str(sdp)))
|
|
458
|
+
await ice.add_remote_candidate(None)
|
|
459
|
+
await asyncio.wait_for(ice.connect(), ICE_SECONDS)
|
|
460
|
+
peer = peer_address(ice)
|
|
461
|
+
protocol = _Protocol(QuicConnection(configuration=configuration))
|
|
462
|
+
transport = _IceTransport(ice)
|
|
463
|
+
protocol.connection_made(transport)
|
|
464
|
+
tasks = [
|
|
465
|
+
asyncio.ensure_future(self._pump(ice, protocol, peer)),
|
|
466
|
+
asyncio.ensure_future(self._watch(protocol)),
|
|
467
|
+
asyncio.ensure_future(self._watch_sleep()),
|
|
468
|
+
]
|
|
469
|
+
protocol.connect(peer)
|
|
470
|
+
try:
|
|
471
|
+
await asyncio.wait_for(protocol.wait_connected(), QUIC_SECONDS)
|
|
472
|
+
except ConnectionError:
|
|
473
|
+
raise ConnectionError(f"QUIC handshake refused: {protocol.terminated}") from None
|
|
474
|
+
if self._protocol is not None:
|
|
475
|
+
raise PathError("the fixed port connected first")
|
|
476
|
+
except BaseException:
|
|
477
|
+
await cancel_tasks(tasks) # before the close, or _watch reports it as a death
|
|
478
|
+
if transport is not None:
|
|
479
|
+
protocol.close()
|
|
480
|
+
transport.close()
|
|
481
|
+
await close_ice(ice)
|
|
482
|
+
self._ice = None
|
|
483
|
+
raise
|
|
484
|
+
self._transport, self._tasks = transport, tasks
|
|
441
485
|
self._protocol, self.remote = protocol, peer
|
|
442
486
|
|
|
443
487
|
async def _dial_fixed(self, answer: dict, configuration: QuicConfiguration) -> None:
|
|
@@ -457,6 +501,8 @@ class DirectPath:
|
|
|
457
501
|
reply = json.loads(await asyncio.wait_for(read_frame(reader), FIXED_SECONDS))
|
|
458
502
|
if not reply.get("ok"):
|
|
459
503
|
raise PathError(str(reply.get("a", "session refused")))
|
|
504
|
+
if self._protocol is not None:
|
|
505
|
+
raise PathError("the punch connected first")
|
|
460
506
|
except BaseException:
|
|
461
507
|
protocol.close()
|
|
462
508
|
transport.close()
|
|
@@ -595,21 +641,30 @@ def _stored_login() -> Optional[dict]:
|
|
|
595
641
|
return homes.recall_login(profile.home) if profile.home else None
|
|
596
642
|
|
|
597
643
|
|
|
598
|
-
def
|
|
599
|
-
"""
|
|
644
|
+
def _active_route(profile) -> Optional[dict]:
|
|
645
|
+
"""The active home's route this process is connected over, if it has one."""
|
|
600
646
|
from ..deployment import homes
|
|
601
|
-
from .grpc_client import _current_profile
|
|
602
647
|
|
|
603
|
-
profile = _current_profile()
|
|
604
648
|
if not profile.home:
|
|
605
649
|
return None
|
|
606
|
-
|
|
650
|
+
return next(
|
|
607
651
|
(r for r in homes.load_home(profile.home)["routes"]
|
|
608
652
|
if f"{r['host']}:{r['port']}" == profile.grpc_endpoint),
|
|
609
653
|
None,
|
|
610
654
|
)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _from_stored_login() -> Optional[DirectPath]:
|
|
658
|
+
"""A path for a process that never logged in (a script using a driver)."""
|
|
659
|
+
from ..deployment import homes
|
|
660
|
+
from .grpc_client import _current_profile
|
|
661
|
+
|
|
662
|
+
profile = _current_profile()
|
|
663
|
+
route = _active_route(profile)
|
|
664
|
+
if route is None or route["kind"] != "relay":
|
|
665
|
+
return None
|
|
611
666
|
login = homes.recall_login(profile.home)
|
|
612
|
-
if
|
|
667
|
+
if login is None:
|
|
613
668
|
return None
|
|
614
669
|
return DirectPath(
|
|
615
670
|
offer=_offer_rpc(login["username"], login["secret"]), server_name=route["host"],
|
|
@@ -690,7 +745,17 @@ def describe() -> str:
|
|
|
690
745
|
if not enabled():
|
|
691
746
|
return "relay (direct path disabled: REMOTERF_DIRECT=0)"
|
|
692
747
|
path = _path
|
|
693
|
-
|
|
748
|
+
if path is not None:
|
|
749
|
+
return path.describe()
|
|
750
|
+
try:
|
|
751
|
+
from .grpc_client import _current_profile
|
|
752
|
+
|
|
753
|
+
route = _active_route(_current_profile())
|
|
754
|
+
except Exception: # noqa: BLE001 - not configured, no home: nothing to add
|
|
755
|
+
route = None
|
|
756
|
+
if route is None or route["kind"] == "relay":
|
|
757
|
+
return "relay (direct path off)"
|
|
758
|
+
return "LAN (direct)" if route["kind"] == "lan" else str(route["kind"])
|
|
694
759
|
|
|
695
760
|
|
|
696
761
|
def stop() -> None:
|
|
@@ -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)."""
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
16
|
import os
|
|
17
|
-
import sys
|
|
18
17
|
from dataclasses import dataclass
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
from typing import Any, Dict, Optional
|
|
@@ -31,7 +30,7 @@ _CONFIG_HELP = (
|
|
|
31
30
|
"Run:\n"
|
|
32
31
|
" remoterf --config --addr <host:port>\n"
|
|
33
32
|
"Example:\n"
|
|
34
|
-
" remoterf --config --addr
|
|
33
|
+
" remoterf --config --addr 203.0.113.10:12321"
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
|
|
@@ -229,10 +228,11 @@ def rpc_client(*, function_name, args, connection=None):
|
|
|
229
228
|
ref = _federated_ref(function_name, args) if connection is None else None
|
|
230
229
|
if ref is not None:
|
|
231
230
|
return _finish(_federated_call(ref, function_name, args))
|
|
232
|
-
wire = os.getenv("REMOTERF_RX_WIRE", "").strip().lower()
|
|
231
|
+
wire = os.getenv("REMOTERF_RX_WIRE", "i16").strip().lower()
|
|
233
232
|
if wire and function_name.endswith(":rx:CALL0"):
|
|
234
|
-
# Gate I slice 3
|
|
235
|
-
# them when they are integer IQ; the driver still sees a complex
|
|
233
|
+
# Gate I slice 3: c64 halves the samples on the wire, i16 (the default)
|
|
234
|
+
# quarters them when they are integer IQ; the driver still sees a complex
|
|
235
|
+
# array. REMOTERF_RX_WIRE="" or "c128" opts out (servers ignore unknown).
|
|
236
236
|
args = {**args, "wire": map_arg(wire)}
|
|
237
237
|
if connection is None and direct_path.wants(function_name):
|
|
238
238
|
path = direct_path.current(token=_device_token(function_name, args))
|
|
@@ -265,9 +265,7 @@ def _finish(response):
|
|
|
265
265
|
|
|
266
266
|
if 'UE' in response.results:
|
|
267
267
|
print(f"UserError: {unmap_arg(response.results['UE'])}")
|
|
268
|
-
|
|
269
|
-
input("Hit enter to continue...")
|
|
270
|
-
|
|
268
|
+
|
|
271
269
|
if 'Message' in response.results:
|
|
272
270
|
print(f"{unmap_arg(response.results['Message'])}")
|
|
273
271
|
|
|
@@ -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)
|