RemoteRF 2.5.2__tar.gz → 2.6.1__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.5.2/src/remoterf.egg-info → remoterf-2.6.1}/PKG-INFO +20 -1
- {remoterf-2.5.2 → remoterf-2.6.1}/README.md +19 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/pyproject.toml +1 -1
- remoterf-2.6.1/src/remoterf/__init__.py +22 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/app.py +4 -3
- remoterf-2.6.1/src/remoterf/core/availability.py +185 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/backend.py +13 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/http.py +6 -2
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +2 -2
- {remoterf-2.5.2 → remoterf-2.6.1/src/remoterf.egg-info}/PKG-INFO +20 -1
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.egg-info/SOURCES.txt +2 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_device_routes.py +1 -1
- remoterf-2.6.1/tests/test_devices_api.py +228 -0
- remoterf-2.5.2/src/remoterf/drivers/support/__init__.py +0 -14
- {remoterf-2.5.2 → remoterf-2.6.1}/LICENSE +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/setup.cfg +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/setup.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/_remoterf_alias.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/grpc.proto +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/grpc/v2_codec.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/tos_notice.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/ansi_codes.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/api_token.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/banner.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/list_string.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/common/utils/process_arg.py +0 -0
- {remoterf-2.5.2/src/remoterf → remoterf-2.6.1/src/remoterf/config}/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/config/cert_fetcher.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/config/config.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/acc_login.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/banner_note.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/direct_path.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/grpc_acc.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/grpc_client.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/secure_channel.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/core/v2_errors.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/direct.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/homes.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/deployment/state.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/_virtual.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/dynamic_device.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/dynamic_v2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.5.2/src/remoterf/config → remoterf-2.6.1/src/remoterf/drivers/support}/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/support/uhd.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/usrp/__init__.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/remoterf_cli.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf/version.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/src/remoterf.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_banner.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_batch.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_bundled_drivers.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_cli_login_fresh.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_config_address.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_direct_path_global.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_direct_path_race.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_federated_devices.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_fetch_idl_fallback.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_finish_prompt.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_homes.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_native_devices.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_old_import_name.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_process_arg.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_relay_paging.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_remoterf_cli.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_rx_readahead.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.5.2 → remoterf-2.6.1}/tests/test_upgrade_rescue.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
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
|
|
@@ -354,6 +354,25 @@ If converting a existing `non-remoterf` compatible script:
|
|
|
354
354
|
|
|
355
355
|
Nothing else needs changing!
|
|
356
356
|
|
|
357
|
+
#### List devices and availability
|
|
358
|
+
|
|
359
|
+
With a saved login (lab or Global), list the devices you can use, each with the handle to paste into `token=`, and whether it is free right now:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import remoterf
|
|
363
|
+
|
|
364
|
+
rows = remoterf.devices() # prints a table and returns it as a list of dicts
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
DEVICE (token=) TYPE ONLINE NOW NEXT
|
|
369
|
+
Pluto_01@lab pluto online free 14:30
|
|
370
|
+
Pluto_02@lab pluto online in use until 13:45 -
|
|
371
|
+
Pluto_03@lab pluto offline yours until 15:00 -
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Each account may ask once every 10 seconds; a repeat call within that reprints the last table with the remaining wait.
|
|
375
|
+
|
|
357
376
|
## Closing
|
|
358
377
|
|
|
359
378
|
This is fundamentally a experimental platform, and there will be many unknown bugs and issues. Some devices do not have universal support for all its functions at the moment, I am working on that aspect.
|
|
@@ -329,6 +329,25 @@ If converting a existing `non-remoterf` compatible script:
|
|
|
329
329
|
|
|
330
330
|
Nothing else needs changing!
|
|
331
331
|
|
|
332
|
+
#### List devices and availability
|
|
333
|
+
|
|
334
|
+
With a saved login (lab or Global), list the devices you can use, each with the handle to paste into `token=`, and whether it is free right now:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
import remoterf
|
|
338
|
+
|
|
339
|
+
rows = remoterf.devices() # prints a table and returns it as a list of dicts
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
DEVICE (token=) TYPE ONLINE NOW NEXT
|
|
344
|
+
Pluto_01@lab pluto online free 14:30
|
|
345
|
+
Pluto_02@lab pluto online in use until 13:45 -
|
|
346
|
+
Pluto_03@lab pluto offline yours until 15:00 -
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Each account may ask once every 10 seconds; a repeat call within that reprints the last table with the remaining wait.
|
|
350
|
+
|
|
332
351
|
## Closing
|
|
333
352
|
|
|
334
353
|
This is fundamentally a experimental platform, and there will be many unknown bugs and issues. Some devices do not have universal support for all its functions at the moment, I am working on that aspect.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoterf"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.6.1"
|
|
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"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (C) 2026 RemoteRF
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def devices():
|
|
18
|
+
"""Print the devices your saved login can use (handles to paste into
|
|
19
|
+
``Pluto(token=...)``) with who-has-it-now availability; returns the rows."""
|
|
20
|
+
from .core.availability import devices as _devices
|
|
21
|
+
|
|
22
|
+
return _devices()
|
|
@@ -567,7 +567,7 @@ def _federated_devices():
|
|
|
567
567
|
deployment = dev["deployment_name"]
|
|
568
568
|
printf(f" @{deployment}", (Sty.BOLD, Sty.MAGENTA))
|
|
569
569
|
state = "online" if dev["online"] else "offline"
|
|
570
|
-
printf("
|
|
570
|
+
printf(f" ({state}) ", Sty.GREEN if dev["online"] else Sty.GRAY, f"{dev['display_name']}{_handle_tag(dev)}", Sty.DEFAULT)
|
|
571
571
|
if data.get("incomplete"):
|
|
572
572
|
printf(f"{data['omitted_count']} more deployment(s) were not queried.", Sty.WARNING)
|
|
573
573
|
return data
|
|
@@ -939,8 +939,9 @@ def _print_native_devices(rows, *, numbered=False):
|
|
|
939
939
|
lab = row["lab"]
|
|
940
940
|
printf(f" @{lab}", (Sty.BOLD, Sty.MAGENTA))
|
|
941
941
|
prefix = (f"{index}. ", Sty.CYAN) if numbered else ()
|
|
942
|
-
|
|
943
|
-
|
|
942
|
+
state = row["state"] or ""
|
|
943
|
+
printf(*prefix, (" " if not numbered else "") + (f"({state}) " if state else ""),
|
|
944
|
+
Sty.GREEN if state.startswith("online") else Sty.GRAY, row["name"], Sty.DEFAULT)
|
|
944
945
|
|
|
945
946
|
|
|
946
947
|
def devices():
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Copyright (C) 2026 RemoteRF
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""remoterf.devices(): the devices the saved login can use, with handles and
|
|
17
|
+
what is free right now. Homes allow one call per account per interval; within
|
|
18
|
+
it this process reprints its last answer instead of asking again."""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import time
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
from ..common.utils import map_arg, unmap_arg
|
|
25
|
+
|
|
26
|
+
_cache = None # (key, monotonic at fetch, rows or None after a refusal, interval)
|
|
27
|
+
NOT_LOGGED_IN = "Not logged in: run `remoterf` and log in (keep the login saved)."
|
|
28
|
+
STALE_LOGIN = "Saved login is no longer valid: run `remoterf` and log in again."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Refused(Exception):
|
|
32
|
+
def __init__(self, wait):
|
|
33
|
+
self.wait = wait
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _when(ts):
|
|
37
|
+
try:
|
|
38
|
+
return datetime.fromtimestamp(ts) if ts else None
|
|
39
|
+
except (OverflowError, ValueError, OSError, TypeError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _hhmm(dt):
|
|
44
|
+
return dt.strftime("%H:%M") if dt.date() == datetime.now().date() else dt.strftime("%b %d %H:%M")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _row(handle, name, dtype, lab, online, state, until, nxt):
|
|
48
|
+
return {"handle": handle, "name": name, "type": dtype, "lab": lab, "online": bool(online),
|
|
49
|
+
"state": state if state in ("free", "busy", "mine") else "unknown",
|
|
50
|
+
"until": _when(until), "next": _when(nxt)}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _global(backend):
|
|
54
|
+
from .app import _global_slug
|
|
55
|
+
from ..deployment.http import AccountBackendError
|
|
56
|
+
|
|
57
|
+
note = None
|
|
58
|
+
try:
|
|
59
|
+
data = backend.availability()
|
|
60
|
+
except AccountBackendError as exc:
|
|
61
|
+
if exc.retry_after:
|
|
62
|
+
raise _Refused(exc.retry_after) from None
|
|
63
|
+
if exc.code != "not_found":
|
|
64
|
+
raise
|
|
65
|
+
data, note = backend.devices(), "This Global home does not report availability yet; showing devices only."
|
|
66
|
+
if data.get("incomplete") or any(d.get("status") == "unavailable" for d in data["destinations"]):
|
|
67
|
+
note = "\n".join(filter(None, [note, "Some labs did not answer; the list may be incomplete."]))
|
|
68
|
+
slugs, rows = {}, []
|
|
69
|
+
for d in data["devices"]:
|
|
70
|
+
key = (d.get("deployment_id"), d.get("deployment_name"))
|
|
71
|
+
if key not in slugs:
|
|
72
|
+
slugs[key] = _global_slug(d.get("device_id", ""), d.get("deployment_name"))
|
|
73
|
+
slug = slugs[key]
|
|
74
|
+
handle = f"{d['handle']}@{slug}" if d.get("handle") and slug else None
|
|
75
|
+
rows.append(_row(handle, d.get("display_name"), d.get("device_type"), d.get("deployment_name"),
|
|
76
|
+
d.get("online"), d.get("state"), d.get("until"), d.get("next")))
|
|
77
|
+
return rows, data.get("interval"), note
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _lab(home, creds):
|
|
81
|
+
from .grpc_client import rpc_client
|
|
82
|
+
|
|
83
|
+
args = {"un": map_arg(creds[0]), "pw": map_arg(creds[1])}
|
|
84
|
+
res = rpc_client(function_name="ACC:availability", args=args).results
|
|
85
|
+
if "UC" in res:
|
|
86
|
+
data = json.loads(unmap_arg(res["UC"]))
|
|
87
|
+
rows = [_row(f"{d['handle']}@{home}" if d.get("handle") and home else None, d.get("display_name"),
|
|
88
|
+
d.get("device_type"), home, d.get("online"), d.get("state"), d.get("until"), d.get("next"))
|
|
89
|
+
for d in data["devices"]]
|
|
90
|
+
return rows, data.get("interval"), None
|
|
91
|
+
if "retry_after" in res:
|
|
92
|
+
raise _Refused(int(unmap_arg(res["retry_after"])))
|
|
93
|
+
error = str(unmap_arg(res.get("ace", map_arg("Unexpected reply from the lab."))))
|
|
94
|
+
if error == "Invalid login credentials.":
|
|
95
|
+
raise RuntimeError(STALE_LOGIN)
|
|
96
|
+
if "No matching server function" not in error:
|
|
97
|
+
raise RuntimeError(error)
|
|
98
|
+
# A lab older than ACC:availability: its device list, no availability.
|
|
99
|
+
from .app import _LOCAL_LABEL, _PARTNER_LABEL, _name_and_handle
|
|
100
|
+
|
|
101
|
+
res = rpc_client(function_name="ACC:get_dev", args=args).results
|
|
102
|
+
if "ace" in res:
|
|
103
|
+
raise RuntimeError(str(unmap_arg(res["ace"])))
|
|
104
|
+
rows = []
|
|
105
|
+
for key in sorted(res, key=lambda k: int(k) if str(k).isdigit() else 0):
|
|
106
|
+
label = str(unmap_arg(res[key]))
|
|
107
|
+
local = _LOCAL_LABEL.match(label)
|
|
108
|
+
if _PARTNER_LABEL.match(label) or not local:
|
|
109
|
+
continue # partner devices have no handles yet
|
|
110
|
+
name, handle = _name_and_handle(local["name"])
|
|
111
|
+
rows.append(_row(f"{handle}@{home}" if handle and home else None, name, None, home,
|
|
112
|
+
local["state"].startswith("online"), "unknown", None, None))
|
|
113
|
+
return rows, None, "This lab does not report availability yet; showing devices only."
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _print(rows, footer=None):
|
|
117
|
+
def now(r):
|
|
118
|
+
if r["state"] == "free":
|
|
119
|
+
return "free"
|
|
120
|
+
if r["state"] in ("busy", "mine") and r["until"]:
|
|
121
|
+
return f"{'yours' if r['state'] == 'mine' else 'in use'} until {_hhmm(r['until'])}"
|
|
122
|
+
return "?"
|
|
123
|
+
|
|
124
|
+
table = [("DEVICE (token=)", "TYPE", "ONLINE", "NOW", "NEXT")] + [
|
|
125
|
+
(r["handle"] or f"{r['name']}{' @' + r['lab'] if r['lab'] else ''} (no handle)", r["type"] or "-", "online" if r["online"] else "offline",
|
|
126
|
+
now(r), _hhmm(r["next"]) if r["next"] else "-") for r in rows]
|
|
127
|
+
widths = [max(len(str(line[i])) for line in table) for i in range(5)]
|
|
128
|
+
for line in table:
|
|
129
|
+
print(" ".join(str(c).ljust(w) for c, w in zip(line, widths)).rstrip())
|
|
130
|
+
if not rows:
|
|
131
|
+
print("(no devices)")
|
|
132
|
+
if footer:
|
|
133
|
+
print(footer)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def devices():
|
|
137
|
+
global _cache
|
|
138
|
+
from ..deployment.state import load_target
|
|
139
|
+
|
|
140
|
+
target = load_target()
|
|
141
|
+
try:
|
|
142
|
+
if target and target["transport"] == "https-json":
|
|
143
|
+
from .grpc_client import federated_backend
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
backend = federated_backend()
|
|
147
|
+
except RuntimeError as exc:
|
|
148
|
+
print(exc)
|
|
149
|
+
return []
|
|
150
|
+
key, fetch = ("global", target["origin"]), lambda: _global(backend)
|
|
151
|
+
else:
|
|
152
|
+
from . import direct_path
|
|
153
|
+
from .grpc_client import _active_home_name
|
|
154
|
+
|
|
155
|
+
home, creds = _active_home_name(), direct_path._credentials()
|
|
156
|
+
if not creds:
|
|
157
|
+
print(NOT_LOGGED_IN)
|
|
158
|
+
return []
|
|
159
|
+
key, fetch = ("lab", home, creds[0]), lambda: _lab(home, creds)
|
|
160
|
+
|
|
161
|
+
if _cache and _cache[0] == key:
|
|
162
|
+
left = _cache[3] - (time.monotonic() - _cache[1])
|
|
163
|
+
if left > 0:
|
|
164
|
+
if _cache[2] is None:
|
|
165
|
+
print(f"Asked too recently; try again in {max(1, round(left))} s.")
|
|
166
|
+
return []
|
|
167
|
+
_print(_cache[2], f"(cached; refresh in {max(1, round(left))} s)")
|
|
168
|
+
return _cache[2]
|
|
169
|
+
try:
|
|
170
|
+
rows, interval, note = fetch()
|
|
171
|
+
except _Refused as exc:
|
|
172
|
+
# Remember the refusal so calls inside the wait stay local.
|
|
173
|
+
old = _cache[2] if _cache and _cache[0] == key else None
|
|
174
|
+
_cache = (key, time.monotonic(), old, float(exc.wait))
|
|
175
|
+
if old is not None:
|
|
176
|
+
_print(old, f"(cached; refresh in {exc.wait} s)")
|
|
177
|
+
return old
|
|
178
|
+
print(f"Asked too recently; try again in {exc.wait} s.")
|
|
179
|
+
return []
|
|
180
|
+
except RuntimeError as exc:
|
|
181
|
+
print(exc)
|
|
182
|
+
return []
|
|
183
|
+
_cache = (key, time.monotonic(), rows, float(interval or 10))
|
|
184
|
+
_print(rows, note)
|
|
185
|
+
return rows
|
|
@@ -325,6 +325,19 @@ class HttpsJsonAccountBackend(DeploymentAccountBackend):
|
|
|
325
325
|
raise AccountBackendError("Invalid home device response")
|
|
326
326
|
return value
|
|
327
327
|
|
|
328
|
+
def availability(self):
|
|
329
|
+
"""Devices plus now/until/next state (Global >= the availability
|
|
330
|
+
route; an older one answers code not_found)."""
|
|
331
|
+
self._operation("ACC:get_dev")
|
|
332
|
+
value = self.transport.request(
|
|
333
|
+
"GET", self.base + "/availability", access=self._access(),
|
|
334
|
+
overall_timeout=DEVICE_AGGREGATION_TIMEOUT_SECONDS + 3,
|
|
335
|
+
max_response_bytes=MAX_DEVICE_LIST_HTTP_RESPONSE_BYTES,
|
|
336
|
+
)
|
|
337
|
+
if type(value.get("devices")) is not list or type(value.get("destinations")) is not list:
|
|
338
|
+
raise AccountBackendError("Invalid home device response")
|
|
339
|
+
return value
|
|
340
|
+
|
|
328
341
|
def reservations(self):
|
|
329
342
|
self._operation("ACC:get_res")
|
|
330
343
|
value = self.transport.request(
|
|
@@ -21,9 +21,10 @@ FRAMED_CONTENT_TYPE = "application/x-remoterf-framed"
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class AccountBackendError(RuntimeError):
|
|
24
|
-
def __init__(self, code, *, provenance=None):
|
|
24
|
+
def __init__(self, code, *, provenance=None, retry_after=None):
|
|
25
25
|
self.code = code
|
|
26
26
|
self.provenance = provenance
|
|
27
|
+
self.retry_after = retry_after
|
|
27
28
|
super().__init__(code)
|
|
28
29
|
|
|
29
30
|
|
|
@@ -120,7 +121,10 @@ class JsonTransport:
|
|
|
120
121
|
)
|
|
121
122
|
if provenance not in {None, "home", "transport", "destination"}:
|
|
122
123
|
provenance = None
|
|
123
|
-
|
|
124
|
+
wait = error.get("retry_after") if isinstance(error, dict) else None
|
|
125
|
+
if type(wait) is not int or not 1 <= wait <= 3600:
|
|
126
|
+
wait = None
|
|
127
|
+
raise AccountBackendError(code, provenance=provenance, retry_after=wait)
|
|
124
128
|
if type(value) is not dict:
|
|
125
129
|
raise AccountBackendError("Invalid account response")
|
|
126
130
|
return value
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
16
|
# Auto-generated from IDL schema — do not edit by hand.
|
|
17
|
-
# device_type: adalm_pluto driver_version: 0.0.
|
|
17
|
+
# device_type: adalm_pluto driver_version: 0.0.4 schema_hash: sha256:9cbbbbc0cd76fc7897dd702adc0c4a0aa9b5042484c6cb38d0057ca217cb92ce
|
|
18
18
|
|
|
19
19
|
_PREFIX = "Adalm_pluto"
|
|
20
|
-
_SCHEMA_HASH = "sha256:
|
|
20
|
+
_SCHEMA_HASH = "sha256:9cbbbbc0cd76fc7897dd702adc0c4a0aa9b5042484c6cb38d0057ca217cb92ce"
|
|
21
21
|
_CLIENT_MODULES = {}
|
|
22
22
|
_CLIENT_OBJECTS = {}
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
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
|
|
@@ -354,6 +354,25 @@ If converting a existing `non-remoterf` compatible script:
|
|
|
354
354
|
|
|
355
355
|
Nothing else needs changing!
|
|
356
356
|
|
|
357
|
+
#### List devices and availability
|
|
358
|
+
|
|
359
|
+
With a saved login (lab or Global), list the devices you can use, each with the handle to paste into `token=`, and whether it is free right now:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import remoterf
|
|
363
|
+
|
|
364
|
+
rows = remoterf.devices() # prints a table and returns it as a list of dicts
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
DEVICE (token=) TYPE ONLINE NOW NEXT
|
|
369
|
+
Pluto_01@lab pluto online free 14:30
|
|
370
|
+
Pluto_02@lab pluto online in use until 13:45 -
|
|
371
|
+
Pluto_03@lab pluto offline yours until 15:00 -
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Each account may ask once every 10 seconds; a repeat call within that reprints the last table with the remaining wait.
|
|
375
|
+
|
|
357
376
|
## Closing
|
|
358
377
|
|
|
359
378
|
This is fundamentally a experimental platform, and there will be many unknown bugs and issues. Some devices do not have universal support for all its functions at the moment, I am working on that aspect.
|
|
@@ -35,6 +35,7 @@ src/remoterf/config/config.py
|
|
|
35
35
|
src/remoterf/core/__init__.py
|
|
36
36
|
src/remoterf/core/acc_login.py
|
|
37
37
|
src/remoterf/core/app.py
|
|
38
|
+
src/remoterf/core/availability.py
|
|
38
39
|
src/remoterf/core/banner_note.txt
|
|
39
40
|
src/remoterf/core/direct_path.py
|
|
40
41
|
src/remoterf/core/dynamic_v2_transport.py
|
|
@@ -77,6 +78,7 @@ tests/test_cli_login_fresh.py
|
|
|
77
78
|
tests/test_config_address.py
|
|
78
79
|
tests/test_deployment_accounts.py
|
|
79
80
|
tests/test_device_routes.py
|
|
81
|
+
tests/test_devices_api.py
|
|
80
82
|
tests/test_direct_path_fixed.py
|
|
81
83
|
tests/test_direct_path_global.py
|
|
82
84
|
tests/test_direct_path_partner.py
|
|
@@ -618,7 +618,7 @@ def test_resdev_n_lists_numbered_devices(monkeypatch):
|
|
|
618
618
|
lines = _shown(monkeypatch, client_app)
|
|
619
619
|
account, _ = _native_shell(monkeypatch, client_app, ["1", "2026-01-01 10:00", "2026-01-01 11:00"])
|
|
620
620
|
client_app.reserve()
|
|
621
|
-
assert "1.
|
|
621
|
+
assert "1. (online) Pluto SDR (OTA) [PlutoA]" in lines
|
|
622
622
|
assert _row(lines, "Device", "handle", "PlutoA@ucla")
|
|
623
623
|
assert account.reserve_device.call_args.args[0] == 3
|
|
624
624
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""remoterf.devices(): handles + availability from the saved login's home."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import remoterf
|
|
10
|
+
from remoterf.common.utils import map_arg
|
|
11
|
+
from remoterf.core import app, availability, direct_path, grpc_client
|
|
12
|
+
from remoterf.deployment import state
|
|
13
|
+
from remoterf.deployment.http import AccountBackendError
|
|
14
|
+
|
|
15
|
+
NOW = int(time.time())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def fresh(monkeypatch):
|
|
20
|
+
monkeypatch.setattr(availability, "_cache", None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _lab(monkeypatch, replies, creds=("jdoe", "s3cret")):
|
|
24
|
+
calls = []
|
|
25
|
+
monkeypatch.setattr(state, "load_target", lambda: {"origin": None, "transport": "grpc", "deployment_id": None})
|
|
26
|
+
monkeypatch.setattr(grpc_client, "_active_home_name", lambda: "ucla")
|
|
27
|
+
monkeypatch.setattr(direct_path, "_credentials", lambda: creds)
|
|
28
|
+
|
|
29
|
+
def rpc(*, function_name, args):
|
|
30
|
+
calls.append(function_name)
|
|
31
|
+
return SimpleNamespace(results=replies[function_name])
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(grpc_client, "rpc_client", rpc)
|
|
34
|
+
return calls
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ok(devices, interval=10):
|
|
38
|
+
return {"UC": map_arg(json.dumps({"now": NOW, "interval": interval, "window": 21600, "devices": devices}))}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
LAB_DEVICES = [
|
|
42
|
+
{"local_device_id": "1", "display_name": "Pluto 01", "device_type": "pluto", "online": True,
|
|
43
|
+
"handle": "Pluto_01", "state": "free", "until": None, "next": NOW + 3600},
|
|
44
|
+
{"local_device_id": "2", "display_name": "Pluto 02", "device_type": "pluto", "online": True,
|
|
45
|
+
"handle": "Pluto_02", "state": "busy", "until": NOW + 600, "next": None},
|
|
46
|
+
{"local_device_id": "3", "display_name": "bench", "device_type": None, "online": False,
|
|
47
|
+
"state": "mine", "until": NOW + 900, "next": None},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_lab_table_and_rows(monkeypatch, capsys):
|
|
52
|
+
calls = _lab(monkeypatch, {"ACC:availability": _ok(LAB_DEVICES)})
|
|
53
|
+
rows = remoterf.devices()
|
|
54
|
+
assert calls == ["ACC:availability"]
|
|
55
|
+
assert [(r["handle"], r["state"], r["online"]) for r in rows] == [
|
|
56
|
+
("Pluto_01@ucla", "free", True), ("Pluto_02@ucla", "busy", True), (None, "mine", False)]
|
|
57
|
+
assert rows[1]["until"].timestamp() == NOW + 600
|
|
58
|
+
out = capsys.readouterr().out
|
|
59
|
+
assert "DEVICE (token=)" in out and "Pluto_01@ucla" in out
|
|
60
|
+
assert "in use until" in out and "yours until" in out and "bench @ucla (no handle)" in out
|
|
61
|
+
assert availability._hhmm(rows[0]["next"]) in out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_hhmm_and_bad_epochs():
|
|
65
|
+
from datetime import datetime, timedelta
|
|
66
|
+
|
|
67
|
+
noon = datetime.now().replace(hour=12, minute=5)
|
|
68
|
+
assert availability._hhmm(noon) == "12:05"
|
|
69
|
+
assert availability._hhmm(noon + timedelta(days=1)) == (noon + timedelta(days=1)).strftime("%b %d 12:05")
|
|
70
|
+
assert availability._when(1_760_000_000_000) is None and availability._when(2**62) is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_refusal_is_remembered_locally(monkeypatch, capsys):
|
|
74
|
+
calls = _lab(monkeypatch, {"ACC:availability": {"ace": map_arg("Too many availability requests; try again in 8 s."),
|
|
75
|
+
"retry_after": map_arg(8)}})
|
|
76
|
+
assert remoterf.devices() == [] and remoterf.devices() == []
|
|
77
|
+
assert calls == ["ACC:availability"]
|
|
78
|
+
out = capsys.readouterr().out
|
|
79
|
+
assert out.count("try again in") == 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_stale_saved_login(monkeypatch, capsys):
|
|
83
|
+
_lab(monkeypatch, {"ACC:availability": {"ace": map_arg("Invalid login credentials.")}})
|
|
84
|
+
assert remoterf.devices() == []
|
|
85
|
+
assert "no longer valid" in capsys.readouterr().out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_cached_within_interval_and_server_retry_after(monkeypatch, capsys):
|
|
89
|
+
calls = _lab(monkeypatch, {"ACC:availability": _ok(LAB_DEVICES)})
|
|
90
|
+
first = remoterf.devices()
|
|
91
|
+
assert remoterf.devices() is first
|
|
92
|
+
assert calls == ["ACC:availability"]
|
|
93
|
+
assert "(cached; refresh in" in capsys.readouterr().out
|
|
94
|
+
|
|
95
|
+
# Cache expired locally but the server still refuses: show the cache and its wait.
|
|
96
|
+
availability._cache = (*availability._cache[:3], 0.0)
|
|
97
|
+
_lab(monkeypatch, {"ACC:availability": {"ace": map_arg("Too many availability requests; try again in 4 s."),
|
|
98
|
+
"retry_after": map_arg(4)}})
|
|
99
|
+
assert remoterf.devices() is first
|
|
100
|
+
assert "(cached; refresh in 4 s)" in capsys.readouterr().out
|
|
101
|
+
|
|
102
|
+
availability._cache = None
|
|
103
|
+
assert remoterf.devices() == []
|
|
104
|
+
assert "try again in 4 s" in capsys.readouterr().out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_old_lab_falls_back_to_get_dev(monkeypatch, capsys):
|
|
108
|
+
calls = _lab(monkeypatch, {
|
|
109
|
+
"ACC:availability": {"ace": map_arg("No matching server function to call: availability")},
|
|
110
|
+
"ACC:get_dev": {"3": map_arg("Pluto 01 (online)"), "4": map_arg("Remote @mit (online)")},
|
|
111
|
+
})
|
|
112
|
+
rows = remoterf.devices()
|
|
113
|
+
assert calls == ["ACC:availability", "ACC:get_dev"]
|
|
114
|
+
assert [(r["handle"], r["state"]) for r in rows] == [("Pluto_01@ucla", "unknown")]
|
|
115
|
+
assert "does not report availability" in capsys.readouterr().out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_lab_not_logged_in(monkeypatch, capsys):
|
|
119
|
+
calls = _lab(monkeypatch, {}, creds=None)
|
|
120
|
+
assert remoterf.devices() == []
|
|
121
|
+
assert calls == []
|
|
122
|
+
assert "Not logged in" in capsys.readouterr().out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class FakeBackend:
|
|
126
|
+
def __init__(self, error=None):
|
|
127
|
+
self.error = error
|
|
128
|
+
|
|
129
|
+
def availability(self):
|
|
130
|
+
if self.error:
|
|
131
|
+
raise self.error
|
|
132
|
+
return {"devices": [
|
|
133
|
+
{"device_id": "u1:1", "deployment_id": "u1", "deployment_name": "ucla", "local_device_id": "1",
|
|
134
|
+
"display_name": "Pluto 01", "device_type": "pluto", "online": True, "handle": "Pluto_01",
|
|
135
|
+
"state": "mine", "until": NOW + 60, "next": None},
|
|
136
|
+
{"device_id": "u2:5", "deployment_id": "u2", "deployment_name": "Some Lab", "local_device_id": "5",
|
|
137
|
+
"display_name": "Pluto 05", "device_type": "pluto", "online": True, "handle": "Pluto_05",
|
|
138
|
+
"state": "unknown", "until": None, "next": None},
|
|
139
|
+
], "destinations": [], "interval": 10}
|
|
140
|
+
|
|
141
|
+
def devices(self):
|
|
142
|
+
return {"devices": [{"device_id": "u1:1", "deployment_id": "u1", "deployment_name": "ucla",
|
|
143
|
+
"local_device_id": "1", "display_name": "Pluto 01", "device_type": "pluto",
|
|
144
|
+
"online": False, "handle": "Pluto_01"}], "destinations": []}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _global(monkeypatch, backend):
|
|
148
|
+
monkeypatch.setattr(state, "load_target", lambda: {"origin": "https://global.example.org",
|
|
149
|
+
"transport": "https-json", "deployment_id": "x"})
|
|
150
|
+
if isinstance(backend, Exception):
|
|
151
|
+
def fail():
|
|
152
|
+
raise backend
|
|
153
|
+
monkeypatch.setattr(grpc_client, "federated_backend", fail)
|
|
154
|
+
else:
|
|
155
|
+
monkeypatch.setattr(grpc_client, "federated_backend", lambda: backend)
|
|
156
|
+
monkeypatch.setattr(app, "_global_slug", lambda device_id, name: name if name == "ucla" else None)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_global_rows(monkeypatch, capsys):
|
|
160
|
+
_global(monkeypatch, FakeBackend())
|
|
161
|
+
rows = remoterf.devices()
|
|
162
|
+
assert [(r["handle"], r["state"], r["lab"]) for r in rows] == [
|
|
163
|
+
("Pluto_01@ucla", "mine", "ucla"), (None, "unknown", "Some Lab")]
|
|
164
|
+
out = capsys.readouterr().out
|
|
165
|
+
assert "yours until" in out and "Pluto 05 @Some Lab (no handle)" in out and "incomplete" not in out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_old_global_falls_back_to_devices(monkeypatch, capsys):
|
|
169
|
+
_global(monkeypatch, FakeBackend(AccountBackendError("not_found")))
|
|
170
|
+
rows = remoterf.devices()
|
|
171
|
+
assert [(r["handle"], r["state"], r["online"]) for r in rows] == [("Pluto_01@ucla", "unknown", False)]
|
|
172
|
+
assert "does not report availability" in capsys.readouterr().out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_old_global_over_http_falls_back(monkeypatch, capsys):
|
|
176
|
+
import httpx
|
|
177
|
+
from remoterf_federation_core import local_capabilities
|
|
178
|
+
|
|
179
|
+
from remoterf.deployment.backend import HttpsJsonAccountBackend
|
|
180
|
+
from remoterf.deployment.http import JsonTransport
|
|
181
|
+
|
|
182
|
+
caps = local_capabilities(deployment_id="550e8400-e29b-41d4-a716-446655440000")
|
|
183
|
+
caps.update(account_transport="https-json", account_api_base="/v2/deployment")
|
|
184
|
+
listing = FakeBackend().devices()
|
|
185
|
+
listing["destinations"] = [{"status": "unavailable", "display_name": "Some Lab"}]
|
|
186
|
+
seen = []
|
|
187
|
+
|
|
188
|
+
def handle(request):
|
|
189
|
+
seen.append(request.url.path)
|
|
190
|
+
if request.url.path == "/.well-known/remoterf-deployment":
|
|
191
|
+
return httpx.Response(200, json=caps)
|
|
192
|
+
if request.url.path == "/v2/deployment/devices":
|
|
193
|
+
return httpx.Response(200, json=listing)
|
|
194
|
+
return httpx.Response(404, json={"detail": "Not Found"}) # FastAPI, no such route
|
|
195
|
+
|
|
196
|
+
transport = JsonTransport("https://global.example.org", client=httpx.Client(transport=httpx.MockTransport(handle)))
|
|
197
|
+
backend = HttpsJsonAccountBackend(transport.origin, transport=transport, store=object())
|
|
198
|
+
backend.credentials = {"access_token": "t", "access_expires_at": 2**31}
|
|
199
|
+
_global(monkeypatch, backend)
|
|
200
|
+
rows = remoterf.devices()
|
|
201
|
+
assert seen[1:] == ["/v2/deployment/availability", "/v2/deployment/devices"]
|
|
202
|
+
assert [(r["handle"], r["state"]) for r in rows] == [("Pluto_01@ucla", "unknown")]
|
|
203
|
+
out = capsys.readouterr().out
|
|
204
|
+
assert "does not report availability" in out and "may be incomplete" in out
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_global_rate_limited_without_cache(monkeypatch, capsys):
|
|
208
|
+
_global(monkeypatch, FakeBackend(AccountBackendError("rate_limited", retry_after=7)))
|
|
209
|
+
assert remoterf.devices() == []
|
|
210
|
+
assert "try again in 7 s" in capsys.readouterr().out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_global_not_logged_in(monkeypatch, capsys):
|
|
214
|
+
_global(monkeypatch, RuntimeError("Log in to the account home first (run `remoterf`, then `login`)"))
|
|
215
|
+
assert remoterf.devices() == []
|
|
216
|
+
assert "Log in to the account home first" in capsys.readouterr().out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_http_error_carries_retry_after():
|
|
220
|
+
import httpx
|
|
221
|
+
|
|
222
|
+
from remoterf.deployment.http import JsonTransport
|
|
223
|
+
|
|
224
|
+
body = {"error": {"code": "rate_limited", "retry_after": 9}}
|
|
225
|
+
client = httpx.Client(transport=httpx.MockTransport(lambda request: httpx.Response(429, json=body)))
|
|
226
|
+
with pytest.raises(AccountBackendError) as raised:
|
|
227
|
+
JsonTransport("https://global.example.org", client=client).request("GET", "/v2/deployment/availability")
|
|
228
|
+
assert (raised.value.code, raised.value.retry_after) == ("rate_limited", 9)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2026 RemoteRF
|
|
2
|
-
#
|
|
3
|
-
# This program is free software: you can redistribute it and/or modify
|
|
4
|
-
# it under the terms of the GNU General Public License as published by
|
|
5
|
-
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
-
# (at your option) any later version.
|
|
7
|
-
#
|
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
-
# GNU General Public License for more details.
|
|
12
|
-
#
|
|
13
|
-
# You should have received a copy of the GNU General Public License
|
|
14
|
-
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
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.5.2 → remoterf-2.6.1}/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
|
{remoterf-2.5.2/src/remoterf/config → remoterf-2.6.1/src/remoterf/drivers/support}/__init__.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
|