remoteRF-server 1.7.20__tar.gz → 1.7.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/PKG-INFO +1 -1
  2. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/pyproject.toml +1 -1
  3. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/remote_admin_shell.py +1 -1
  4. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/acc_perms.py +12 -9
  5. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/commands.py +9 -5
  6. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/grpc_server.py +7 -1
  7. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/reservation.py +12 -7
  8. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/user_group_handler.py +1 -1
  9. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/serverrf_cli.py +223 -7
  10. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  11. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_client_sessions.py +57 -8
  12. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_commands.py +153 -0
  13. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/LICENSE +0 -0
  14. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/README.md +0 -0
  15. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/setup.cfg +0 -0
  16. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/__init__.py +0 -0
  17. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/__init__.py +0 -0
  18. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  19. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  20. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  21. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  22. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  23. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  24. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  25. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  26. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  27. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  28. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  29. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  30. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/__init__.py +0 -0
  31. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  32. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  33. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  34. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/schema.py +0 -0
  35. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/idl/v2.py +0 -0
  36. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/__init__.py +0 -0
  37. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  38. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/api_token.py +0 -0
  39. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  40. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/db_location.py +0 -0
  41. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/list_string.py +0 -0
  42. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/panel.py +0 -0
  43. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  44. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/__init__.py +0 -0
  45. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  46. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
  47. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  48. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  49. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  50. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  51. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth/__init__.py +0 -0
  52. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth/bluetooth_schema.py +0 -0
  53. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
  54. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps/__init__.py +0 -0
  55. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps/gps_schema.py +0 -0
  56. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/gps_diag.py +0 -0
  57. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  58. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  59. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/krakensdr/__init__.py +0 -0
  60. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/krakensdr/krakensdr_schema.py +0 -0
  61. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rfbeam_kmd7/__init__.py +0 -0
  62. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rfbeam_kmd7/rfbeam_kmd7_schema.py +0 -0
  63. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  64. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
  65. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  66. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_awr6843/__init__.py +0 -0
  67. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_awr6843/ti_awr6843_schema.py +0 -0
  68. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  69. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  70. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  71. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  72. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  73. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  74. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
  75. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  76. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  77. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  78. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/wifi/__init__.py +0 -0
  79. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/drivers/wifi/wifi_schema.py +0 -0
  80. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/__init__.py +0 -0
  81. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_auth_token.py +0 -0
  82. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_directory_store.py +0 -0
  83. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  84. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/__init__.py +0 -0
  85. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/cert_provider.py +0 -0
  86. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/console.py +0 -0
  87. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/control.py +0 -0
  88. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  89. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  90. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/device_manager.py +0 -0
  91. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/diag.py +0 -0
  92. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/direct_path.py +0 -0
  93. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  94. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  95. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/__init__.py +0 -0
  96. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/admin.py +0 -0
  97. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/cli.py +0 -0
  98. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/cluster.py +0 -0
  99. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/contracts.py +0 -0
  100. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/destination.py +0 -0
  101. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/devices.py +0 -0
  102. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  103. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/global_home.py +0 -0
  104. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/home.py +0 -0
  105. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  106. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/identity.py +0 -0
  107. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  108. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/links.py +0 -0
  109. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/migrations.py +0 -0
  110. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/peers.py +0 -0
  111. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/permissions.py +0 -0
  112. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/principals.py +0 -0
  113. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/runtime.py +0 -0
  114. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/federation/trust.py +0 -0
  115. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/global_onboarding.py +0 -0
  116. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/host_direct.py +0 -0
  117. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  118. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/json_output.py +0 -0
  119. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/remote_admin.py +0 -0
  120. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
  121. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/rpc_manager.py +0 -0
  122. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/schema_registry.py +0 -0
  123. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/startup_check.py +0 -0
  124. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/transport_counters.py +0 -0
  125. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/user_group_cli.py +0 -0
  126. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/server/v2_errors.py +0 -0
  127. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/__init__.py +0 -0
  128. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/gen_certs.py +0 -0
  129. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  130. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  131. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  132. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  133. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/requires.txt +0 -0
  134. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  135. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/__init__.py +0 -0
  136. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/cli.py +0 -0
  137. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/dongle.py +0 -0
  138. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/safety.py +0 -0
  139. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/src/rssi/scanner.py +0 -0
  140. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_hardening.py +0 -0
  141. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_names.py +0 -0
  142. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_account_removal.py +0 -0
  143. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_activity_log.py +0 -0
  144. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_availability.py +0 -0
  145. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_bind_host.py +0 -0
  146. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_cert_provider.py +0 -0
  147. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_cluster.py +0 -0
  148. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_console.py +0 -0
  149. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_contrib_drivers.py +0 -0
  150. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_rtl_sdr_schema.py +0 -0
  151. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_usrp_schema.py +0 -0
  152. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_default_wifi_schema.py +0 -0
  153. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_deployment_capabilities.py +0 -0
  154. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_detached_device_rpc.py +0 -0
  155. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_core_aliases.py +0 -0
  156. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_handles.py +0 -0
  157. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_device_init.py +0 -0
  158. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_devices_add_wizard.py +0 -0
  159. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_devices_list.py +0 -0
  160. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_diag.py +0 -0
  161. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_direct_fixed_port.py +0 -0
  162. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_direct_path_server.py +0 -0
  163. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_dynamic_v2.py +0 -0
  164. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federated_direct_path.py +0 -0
  165. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federated_dynamic_v2.py +0 -0
  166. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_federation_edges.py +0 -0
  167. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_first_run.py +0 -0
  168. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_d_contracts.py +0 -0
  169. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_e_federation.py +0 -0
  170. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_e_proto.py +0 -0
  171. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_f_enrollment.py +0 -0
  172. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_g1_logging.py +0 -0
  173. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_g_permissions.py +0 -0
  174. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_h_devices.py +0 -0
  175. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gate_i_direct_path.py +0 -0
  176. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_gen_certs.py +0 -0
  177. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_accounts_migration.py +0 -0
  178. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_home.py +0 -0
  179. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_onboarding.py +0 -0
  180. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_global_v1_disposal.py +0 -0
  181. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_hackrf_schema.py +0 -0
  182. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_home_device_plane.py +0 -0
  183. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_direct.py +0 -0
  184. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_directory_store.py +0 -0
  185. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_host_dynamic_v2.py +0 -0
  186. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_idl_schema.py +0 -0
  187. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_json_commands.py +0 -0
  188. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_lab_logins.py +0 -0
  189. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_native_drivers.py +0 -0
  190. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_panel.py +0 -0
  191. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_peer_transport.py +0 -0
  192. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_pluto_compat.py +0 -0
  193. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_pluto_poe.py +0 -0
  194. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_process_arg.py +0 -0
  195. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_proxy_lifecycle.py +0 -0
  196. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_remote_admin.py +0 -0
  197. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_cancel.py +0 -0
  198. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_handoff.py +0 -0
  199. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_policy_b.py +0 -0
  200. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_reservation_timestamps.py +0 -0
  201. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_batch.py +0 -0
  202. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_calln.py +0 -0
  203. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_host_legacy.py +0 -0
  204. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rpc_manager_rx_n.py +0 -0
  205. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_rtl_sdr_schema.py +0 -0
  206. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_schema_registry.py +0 -0
  207. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_soak_federation.py +0 -0
  208. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_startup_check.py +0 -0
  209. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_ti_mmwave_hardware.py +0 -0
  210. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_ti_mmwave_schema.py +0 -0
  211. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_transport_counters.py +0 -0
  212. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_user_group_cli.py +0 -0
  213. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  214. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_client_server_hardware.py +0 -0
  215. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_client_server_localhost.py +0 -0
  216. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  217. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_preload.py +0 -0
  218. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_profiles.py +0 -0
  219. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  220. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_usrp_uhd410_hardware.py +0 -0
  221. {remoterf_server-1.7.20 → remoterf_server-1.7.22}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.20
3
+ Version: 1.7.22
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.7.20"
7
+ version = "1.7.22"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -75,7 +75,7 @@ TIERS = {
75
75
  DENY_FLAGS = {"--secrets"}
76
76
  # Destructive commands whose own confirmation -y answers (the step-up asked already).
77
77
  TAKES_YES = {"devices wipe", "stats purge", "hosts wipe", "certs wipe", "users prune-proxies",
78
- "hosts tokens", "certs reset"}
78
+ "hosts tokens", "certs reset", "groups remove"}
79
79
  KEEP_BACKUPS = 10
80
80
  PAIR_CODE = re.compile(r"[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}")
81
81
  BUILTINS = ("help", "me", "revoke", "passwd", "log", "clear", "exit")
@@ -243,14 +243,16 @@ class perms_db:
243
243
  )
244
244
 
245
245
  @db_connection
246
- def set_admin(self, username:str):
247
- print(f"Setting {username} as an admin")
248
- from .control import prompt
249
-
250
- inpu = prompt(f"Are you sure you want to set {username} as an admin? \nThis gives them unrestricted access. (Y/N): ")
251
- if inpu != 'Y' and inpu != 'y':
252
- print(f"Admin set is canceled for user: {username}.")
253
- return
246
+ def set_admin(self, username:str, *, confirm: bool = True, quiet: bool = False):
247
+ if not quiet:
248
+ print(f"Setting {username} as an admin")
249
+ if confirm:
250
+ from .control import prompt
251
+
252
+ inpu = prompt(f"Are you sure you want to set {username} as an admin? \nThis gives them unrestricted access. (Y/N): ")
253
+ if inpu != 'Y' and inpu != 'y':
254
+ print(f"Admin set is canceled for user: {username}.")
255
+ return
254
256
 
255
257
 
256
258
  # SQL command to insert a new user
@@ -260,7 +262,8 @@ class perms_db:
260
262
  # Execute the SQL command
261
263
  self.remove_user(username)
262
264
  self.cursor.execute(insert_query, (username,))
263
- print("Admin added successfully")
265
+ if not quiet:
266
+ print("Admin added successfully")
264
267
 
265
268
  except sqlite3.Error as e:
266
269
  print("An error occurred:", e)
@@ -120,7 +120,7 @@ def _table() -> list[Command]:
120
120
  Command(("devices wipe",), "Remove ALL devices [-y]", _shell("cmd_devices_wipe"), "Devices", after="reload_devices"),
121
121
  # ---- users ----
122
122
  Command(("users list",), "List accounts", _daemon("cmd_users_list"), "Users", where=D),
123
- Command(("users manage",), "Change a user's role (interactive)", _daemon("cmd_users_manage"), "Users", where=D, interactive=True),
123
+ Command(("users manage",), "Change a user's role: users manage <user> --role <user|power-user|admin> --yes [--json]; no arguments is interactive", _daemon("cmd_users_manage"), "Users", where=D, interactive=True),
124
124
  Command(("users perms",), "Show permissions", _daemon("cmd_users_perms"), "Users", where=D),
125
125
  Command(("users purge",), "Remove ALL accounts", _daemon("cmd_users_purge"), "Users", where=D),
126
126
  Command(("users prune-proxies",), "Partner-deployment visitors whose home is gone: list them, -y removes [--stale DAYS]", _daemon("cmd_users_prune_proxies"), "Users", where=D),
@@ -131,15 +131,15 @@ def _table() -> list[Command]:
131
131
  # ---- groups ----
132
132
  Command(("groups list",), "List user groups", _shell("cmd_groups_list"), "Groups"),
133
133
  Command(("groups create",), "Create a group: groups create <name> --devices <ids> [--max-reservations 3 --max-time 1800 --horizon 604800 --lifetime 0 --lan-only --public --share <clusters>] --yes [--json]; no name is interactive", _shell("cmd_groups_create"), "Groups"),
134
- Command(("groups edit",), "Edit a user group (interactive)", _shell("cmd_groups_edit"), "Groups"),
135
- Command(("groups remove",), "Delete a user group (interactive)", _shell("cmd_groups_remove"), "Groups"),
134
+ Command(("groups edit",), "Edit a group: groups edit <id|name> [--name N --devices 0,1 --max-reservations N --max-time SEC --lifetime SEC --horizon SEC --lan-only|--no-lan-only --share a,b|none] --yes [--json]; no arguments is interactive", _shell("cmd_groups_edit"), "Groups"),
135
+ Command(("groups remove",), "Delete a group: groups remove <id|name> --yes [--json]; no arguments is interactive", _shell("cmd_groups_remove"), "Groups"),
136
136
  Command(("groups public",), "Share a group with anyone on RemoteRF Global, or stop: groups public <group> on|off", _federation("public"), "Groups"),
137
137
  Command(("groups share",), "Where a group is shared, or change it: groups share <group> [<cluster> on|off]", _federation("share"), "Groups"),
138
138
  Command(("groups csv",), "Export ALL user groups as CSV to Downloads", _shell("cmd_groups_csv"), "Groups"),
139
139
  # ---- enrollment codes ----
140
140
  Command(("codes list",), "List enrollment codes", _shell("cmd_codes_list"), "Enrollment codes"),
141
141
  Command(("codes create",), "Create codes: codes create <group name|id|none> --count N [--length 10 --uses 1 --duration 3600] --yes [--json]; no group is interactive", _shell("cmd_codes_create"), "Enrollment codes"),
142
- Command(("codes remove",), "Delete an enrollment code (interactive)", _shell("cmd_codes_remove"), "Enrollment codes"),
142
+ Command(("codes remove",), "Delete codes: codes remove <code-or-invite> --yes [--json] | codes remove --group <id> --yes [--json]; no arguments is interactive", _shell("cmd_codes_remove"), "Enrollment codes"),
143
143
  Command(("codes csv",), "Export ALL enrollment codes as CSV to Downloads", _shell("cmd_codes_csv"), "Enrollment codes"),
144
144
  # ---- hosts ----
145
145
  Command(("hosts status",), "Live host online/last_seen/devices", _daemon("cmd_hosts_status"), "Hosts", where=D),
@@ -353,7 +353,11 @@ def _run_in_daemon(command: Command, rest: list[str]) -> int:
353
353
  if command.console_only and remote:
354
354
  print(f"`{command.names[0]}` works only at the lab console.", file=sys.stderr)
355
355
  return 1
356
- if command.console_only or (command.interactive and not remote):
356
+ users_manage_flags = command.names[0] == "users manage" and bool(rest)
357
+ if command.names[0] == "users manage" and not rest and not remote and not sys.stdin.isatty():
358
+ print("ERROR: non-interactive users manage requires a user, --role, and --yes", file=sys.stderr)
359
+ return 1
360
+ if command.console_only or (command.interactive and not remote and not users_manage_flags):
357
361
  # Prompts need the console's terminal: type it there and attach. Never
358
362
  # for a remote admin: that would hand them the live console, past the policy.
359
363
  return console.run_in_console([*command.names[0].split(), *rest])
@@ -1371,7 +1371,13 @@ def cmd_clear(argv): clear()
1371
1371
  def cmd_hosts_status(argv): _cmd_hosts_status(json_output="--json" in argv)
1372
1372
  def cmd_hosts_wipe(argv): _cmd_hosts_wipe(yes=("-y" in argv or "--yes" in argv))
1373
1373
  def cmd_users_list(argv): reservation_handler.print_all_accounts()
1374
- def cmd_users_manage(argv): set_acc()
1374
+ def cmd_users_manage(argv):
1375
+ if not argv:
1376
+ set_acc()
1377
+ return 0
1378
+ from remoteRF_server.serverrf_cli import _cmd_users_manage_noninteractive
1379
+
1380
+ return _cmd_users_manage_noninteractive(argv, perms)
1375
1381
  def cmd_users_perms(argv): perms.print_perms()
1376
1382
  def cmd_users_purge(argv): reservation_handler.remove_all_users()
1377
1383
  def cmd_reservations_list(argv): reservation_handler.print_all_reservations(json_output="--json" in argv)
@@ -226,7 +226,8 @@ class ReservationHandler:
226
226
  DROP TRIGGER IF EXISTS reserved_total_del;
227
227
  CREATE TRIGGER reserved_total_ins AFTER INSERT ON Reservations
228
228
  WHEN NEW.device_id < 1000000 BEGIN
229
- UPDATE ReservedTotal SET seconds = seconds + (julianday(NEW.end_time) - julianday(NEW.start_time)) * 86400;
229
+ UPDATE ReservedTotal SET seconds = seconds + MAX(0,
230
+ (julianday(NEW.end_time) - julianday(MAX(NEW.start_time, datetime('now', 'localtime')))) * 86400);
230
231
  END;
231
232
  CREATE TRIGGER reserved_total_del AFTER DELETE ON Reservations
232
233
  WHEN OLD.device_id < 1000000 BEGIN
@@ -264,6 +265,8 @@ class ReservationHandler:
264
265
  day = lambda r: f"'d' || ((CAST(strftime('%w', {r}.start_time, 'utc') AS INTEGER) + 6) % 7)" # noqa: E731
265
266
  utc_date = lambda r: f"date({r}.start_time, 'utc')" # noqa: E731
266
267
  length = lambda r: f"(julianday({r}.end_time) - julianday({r}.start_time)) * 86400" # noqa: E731
268
+ # A booking made after its start counts only from now on.
269
+ held = '''MAX(0, (julianday(NEW.end_time) - julianday(MAX(NEW.start_time, datetime('now', 'localtime')))) * 86400)'''
267
270
  cancel = '''reservations = MAX(reservations - (OLD.start_time > datetime('now', 'localtime')), 0),
268
271
  seconds = MAX(seconds - MAX(0,
269
272
  (julianday(OLD.end_time) - julianday(MAX(OLD.start_time, datetime('now', 'localtime')))) * 86400), 0)'''
@@ -278,12 +281,13 @@ class ReservationHandler:
278
281
  UNION ALL SELECT device_id, {day("r")}, {length("r")} FROM Reservations r)
279
282
  WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationBuckets)
280
283
  GROUP BY device_id, bucket;
281
- CREATE TRIGGER IF NOT EXISTS reservation_buckets_ins AFTER INSERT ON Reservations
284
+ DROP TRIGGER IF EXISTS reservation_buckets_ins;
285
+ CREATE TRIGGER reservation_buckets_ins AFTER INSERT ON Reservations
282
286
  WHEN NEW.device_id < 1000000 BEGIN
283
287
  INSERT INTO ReservationBuckets VALUES
284
- (NEW.device_id, 'all', 1, {length("NEW")}),
285
- (NEW.device_id, {hour("NEW")}, 1, {length("NEW")}),
286
- (NEW.device_id, {day("NEW")}, 1, {length("NEW")})
288
+ (NEW.device_id, 'all', 1, {held}),
289
+ (NEW.device_id, {hour("NEW")}, 1, {held}),
290
+ (NEW.device_id, {day("NEW")}, 1, {held})
287
291
  ON CONFLICT (device_id, bucket) DO UPDATE
288
292
  SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
289
293
  END;
@@ -298,9 +302,10 @@ class ReservationHandler:
298
302
  SELECT device_id, {utc_date("r")} AS day, COUNT(*), SUM({length("r")}) FROM Reservations r
299
303
  WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationDays)
300
304
  GROUP BY device_id, day;
301
- CREATE TRIGGER IF NOT EXISTS reservation_days_ins AFTER INSERT ON Reservations
305
+ DROP TRIGGER IF EXISTS reservation_days_ins;
306
+ CREATE TRIGGER reservation_days_ins AFTER INSERT ON Reservations
302
307
  WHEN NEW.device_id < 1000000 BEGIN
303
- INSERT INTO ReservationDays VALUES (NEW.device_id, {utc_date("NEW")}, 1, {length("NEW")})
308
+ INSERT INTO ReservationDays VALUES (NEW.device_id, {utc_date("NEW")}, 1, {held})
304
309
  ON CONFLICT (device_id, day) DO UPDATE
305
310
  SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
306
311
  END;
@@ -395,7 +395,7 @@ class UserGroupHandler:
395
395
  v = int(lifetime_sec)
396
396
  except Exception:
397
397
  return False
398
- if v <= 0:
398
+ if v < 0:
399
399
  return False
400
400
  sets.append("lifetime_sec = ?")
401
401
  params.append(v)
@@ -1449,11 +1449,15 @@ def _yes(argv: List[str]) -> bool:
1449
1449
 
1450
1450
 
1451
1451
  def _parse_create_args(argv, *, value_flags, bool_flags):
1452
- """Small strict parser for the two forced-command-safe create forms."""
1452
+ """Small strict parser for forced-command-safe flag forms."""
1453
1453
  positional, values = [], {}
1454
1454
  i = 0
1455
1455
  while i < len(argv):
1456
1456
  token = argv[i]
1457
+ if token == "-y" and "yes" in bool_flags:
1458
+ values["yes"] = True
1459
+ i += 1
1460
+ continue
1457
1461
  if not token.startswith("--"):
1458
1462
  positional.append(token)
1459
1463
  i += 1
@@ -2204,15 +2208,141 @@ def cmd_groups_create(argv):
2204
2208
 
2205
2209
 
2206
2210
  def cmd_groups_edit(argv):
2207
- from remoteRF_server.server.user_group_cli import edit_user_group_local
2211
+ from remoteRF_server.server import user_group_cli as groups
2208
2212
 
2209
- edit_user_group_local(_prompt_session())
2213
+ if not argv:
2214
+ if sys.stdin.isatty():
2215
+ groups.edit_user_group_local(_prompt_session())
2216
+ return 0
2217
+ print("ERROR: non-interactive groups edit requires a group and change flags", file=sys.stderr)
2218
+ return 1
2219
+ if not _yes(argv):
2220
+ print("ERROR: non-interactive groups edit requires --yes", file=sys.stderr)
2221
+ return 1
2222
+ parsed, error = _parse_create_args(
2223
+ argv,
2224
+ value_flags={"name", "devices", "max-reservations", "max-time", "lifetime", "horizon", "share"},
2225
+ bool_flags={"yes", "json", "lan-only", "no-lan-only"},
2226
+ )
2227
+ if error:
2228
+ print(f"ERROR: {error}", file=sys.stderr)
2229
+ return 1
2230
+ pos, flags = parsed
2231
+ if len(pos) != 1:
2232
+ print("ERROR: groups edit requires exactly one group id or name", file=sys.stderr)
2233
+ return 1
2234
+ if "lan-only" in flags and "no-lan-only" in flags:
2235
+ print("ERROR: --lan-only and --no-lan-only are mutually exclusive", file=sys.stderr)
2236
+ return 1
2237
+ change_flags = set(flags) - {"yes", "json"}
2238
+ if not change_flags:
2239
+ print("ERROR: groups edit requires at least one change flag", file=sys.stderr)
2240
+ return 1
2241
+
2242
+ all_groups = groups.user_group_handler.get_all_user_groups(include_users=False) or []
2243
+ ref = pos[0].strip()
2244
+ selected = next((g for g in all_groups if str(g.get("group_id")) == ref), None)
2245
+ if selected is None:
2246
+ selected = next((g for g in all_groups if str(g.get("group_name")) == ref), None)
2247
+ if selected is None:
2248
+ print(f"ERROR: unknown group {ref!r}", file=sys.stderr)
2249
+ return 1
2250
+ group_id = int(selected["group_id"])
2251
+
2252
+ try:
2253
+ devices = _comma_ints(flags["devices"]) if "devices" in flags else None
2254
+ max_reservations = (_bounded_int(flags["max-reservations"], "--max-reservations", minimum=1)
2255
+ if "max-reservations" in flags else None)
2256
+ max_time = (_bounded_int(flags["max-time"], "--max-time", minimum=1)
2257
+ if "max-time" in flags else None)
2258
+ lifetime = (_bounded_int(flags["lifetime"], "--lifetime", minimum=0)
2259
+ if "lifetime" in flags else None)
2260
+ horizon = (_bounded_int(flags["horizon"], "--horizon", minimum=0)
2261
+ if "horizon" in flags else None)
2262
+ except ValueError as exc:
2263
+ print(f"ERROR: {exc}", file=sys.stderr)
2264
+ return 1
2265
+ name = flags.get("name")
2266
+ if name is not None and not str(name).strip():
2267
+ print("ERROR: --name must not be empty", file=sys.stderr)
2268
+ return 1
2269
+ lan_only = True if flags.get("lan-only") else False if flags.get("no-lan-only") else None
2270
+
2271
+ sharing = None
2272
+ if "share" in flags:
2273
+ from remoteRF_server.server.federation import cluster
2274
+
2275
+ sharing = groups._names(str(flags["share"]))
2276
+ try:
2277
+ cluster.share_group(str(group_id), sharing)
2278
+ except cluster.ClusterError as exc:
2279
+ print(f"ERROR: sharing unchanged: {exc}", file=sys.stderr)
2280
+ return 1
2281
+
2282
+ fields = (name, devices, max_time, max_reservations, lifetime, horizon, lan_only)
2283
+ if any(value is not None for value in fields):
2284
+ ok = groups.user_group_handler.edit_user_group(
2285
+ group_id=group_id,
2286
+ group_name=name,
2287
+ devices_whitelist=devices,
2288
+ max_reservation_time_sec=max_time,
2289
+ max_reservations=max_reservations,
2290
+ lifetime_sec=lifetime,
2291
+ lan_only=lan_only,
2292
+ max_horizon_sec=horizon,
2293
+ )
2294
+ if not ok:
2295
+ print(f"ERROR: failed to update group {ref!r}", file=sys.stderr)
2296
+ return 1
2297
+
2298
+ updated = next((g for g in groups.user_group_handler.get_all_user_groups(include_users=False) or []
2299
+ if int(g.get("group_id", -1)) == group_id), selected)
2300
+ if flags.get("json"):
2301
+ result = dict(updated)
2302
+ if sharing is not None:
2303
+ result["shared_with"] = sharing
2304
+ print(json.dumps(result, separators=(",", ":"), default=str))
2305
+ else:
2306
+ print(f"Updated group {updated.get('group_name', ref)} (id {group_id})")
2307
+ return 0
2210
2308
 
2211
2309
 
2212
2310
  def cmd_groups_remove(argv):
2213
- from remoteRF_server.server.user_group_cli import remove_user_group_local
2311
+ from remoteRF_server.server import user_group_cli as groups
2214
2312
 
2215
- remove_user_group_local(_prompt_session())
2313
+ if not argv:
2314
+ if sys.stdin.isatty():
2315
+ groups.remove_user_group_local(_prompt_session())
2316
+ return 0
2317
+ print("ERROR: non-interactive groups remove requires a group and --yes", file=sys.stderr)
2318
+ return 1
2319
+ if not _yes(argv):
2320
+ print("ERROR: non-interactive groups remove requires --yes", file=sys.stderr)
2321
+ return 1
2322
+ parsed, error = _parse_create_args(argv, value_flags=set(), bool_flags={"yes", "json"})
2323
+ if error:
2324
+ print(f"ERROR: {error}", file=sys.stderr)
2325
+ return 1
2326
+ pos, flags = parsed
2327
+ if len(pos) != 1:
2328
+ print("ERROR: groups remove requires exactly one group id or name", file=sys.stderr)
2329
+ return 1
2330
+ ref = pos[0].strip()
2331
+ all_groups = groups.user_group_handler.get_all_user_groups(include_users=False) or []
2332
+ selected = next((g for g in all_groups if str(g.get("group_id")) == ref), None)
2333
+ if selected is None:
2334
+ selected = next((g for g in all_groups if str(g.get("group_name")) == ref), None)
2335
+ if selected is None:
2336
+ print(f"ERROR: unknown group {ref!r}", file=sys.stderr)
2337
+ return 1
2338
+ if not groups.user_group_handler.delete_user_group(group_id=int(selected["group_id"])):
2339
+ print(f"ERROR: failed to delete group {ref!r}", file=sys.stderr)
2340
+ return 1
2341
+ if flags.get("json"):
2342
+ print(json.dumps(selected, separators=(",", ":"), default=str))
2343
+ else:
2344
+ print(f"Deleted group {selected['group_name']} (id {selected['group_id']})")
2345
+ return 0
2216
2346
 
2217
2347
 
2218
2348
  def cmd_groups_csv(argv):
@@ -2305,9 +2435,48 @@ def cmd_codes_create(argv):
2305
2435
 
2306
2436
 
2307
2437
  def cmd_codes_remove(argv):
2308
- from remoteRF_server.server.user_group_cli import remove_enrollment_code_local
2438
+ from remoteRF_server.server import user_group_cli as groups
2309
2439
 
2310
- remove_enrollment_code_local(_prompt_session())
2440
+ if not argv:
2441
+ if sys.stdin.isatty():
2442
+ groups.remove_enrollment_code_local(_prompt_session())
2443
+ return 0
2444
+ print("ERROR: non-interactive codes remove requires a code or --group and --yes", file=sys.stderr)
2445
+ return 1
2446
+ if not _yes(argv):
2447
+ print("ERROR: non-interactive codes remove requires --yes", file=sys.stderr)
2448
+ return 1
2449
+ parsed, error = _parse_create_args(argv, value_flags={"group"}, bool_flags={"yes", "json"})
2450
+ if error:
2451
+ print(f"ERROR: {error}", file=sys.stderr)
2452
+ return 1
2453
+ pos, flags = parsed
2454
+ if (len(pos) != 1) == ("group" not in flags):
2455
+ print("ERROR: codes remove requires exactly one code/invite or --group <id>", file=sys.stderr)
2456
+ return 1
2457
+ rows = groups.user_group_handler.get_all_enrollment_codes() or []
2458
+ if "group" in flags:
2459
+ try:
2460
+ group_id = _bounded_int(flags["group"], "--group", minimum=0)
2461
+ except ValueError as exc:
2462
+ print(f"ERROR: {exc}", file=sys.stderr)
2463
+ return 1
2464
+ removed = [row for row in rows if int(row.get("group_id", -1)) == group_id]
2465
+ if not removed or not groups.user_group_handler.delete_enrollment_code_id(group_id=group_id):
2466
+ print(f"ERROR: no enrollment codes found for group {group_id}", file=sys.stderr)
2467
+ return 1
2468
+ result = {"group_id": group_id, "removed": removed}
2469
+ message = f"Deleted {len(removed)} enrollment code(s) for group {group_id}"
2470
+ else:
2471
+ code_name = pos[0].rpartition("/")[2].strip()
2472
+ selected = next((row for row in rows if row.get("code_name") == code_name), None)
2473
+ if selected is None or not groups.user_group_handler.delete_enrollment_code_name(code_name=code_name):
2474
+ print(f"ERROR: enrollment code {code_name!r} not found", file=sys.stderr)
2475
+ return 1
2476
+ result = selected
2477
+ message = f"Deleted enrollment code {code_name}"
2478
+ print(json.dumps(result, separators=(",", ":"), default=str) if flags.get("json") else message)
2479
+ return 0
2311
2480
 
2312
2481
 
2313
2482
  def cmd_codes_csv(argv):
@@ -2316,6 +2485,53 @@ def cmd_codes_csv(argv):
2316
2485
  export_enrollment_codes_csv_local()
2317
2486
 
2318
2487
 
2488
+ def _cmd_users_manage_noninteractive(argv, perms) -> int:
2489
+ """Flag-only half of the daemon's users-manage command."""
2490
+ if "-y" not in argv and "--yes" not in argv:
2491
+ print("ERROR: non-interactive users manage requires --yes", file=sys.stderr)
2492
+ return 1
2493
+ parsed, error = _parse_create_args(
2494
+ argv, value_flags={"role"}, bool_flags={"yes", "json"},
2495
+ )
2496
+ if error:
2497
+ print(f"ERROR: {error}", file=sys.stderr)
2498
+ return 1
2499
+ positional, flags = parsed
2500
+ if len(positional) != 1 or "role" not in flags:
2501
+ print("ERROR: users manage requires exactly one user and --role <user|power-user|admin>", file=sys.stderr)
2502
+ return 1
2503
+
2504
+ username = positional[0]
2505
+ current = perms.get_perms(username)
2506
+ if not current:
2507
+ print(f"ERROR: user {username!r} does not exist", file=sys.stderr)
2508
+ return 1
2509
+ raw_role = str(flags["role"]).strip().lower().replace("_", "-")
2510
+ roles = {
2511
+ "u": "Normal User", "user": "Normal User", "normal-user": "Normal User", "normal user": "Normal User",
2512
+ "p": "Power User", "power": "Power User", "power-user": "Power User", "power user": "Power User",
2513
+ "a": "Admin", "admin": "Admin",
2514
+ }
2515
+ role = roles.get(raw_role)
2516
+ if role is None:
2517
+ print("ERROR: --role must be user, power-user, or admin", file=sys.stderr)
2518
+ return 1
2519
+ if role == "Normal User":
2520
+ perms.set_user(username)
2521
+ elif role == "Admin":
2522
+ perms.set_admin(username, confirm=False, quiet=True)
2523
+ elif current[0][0] != "Power User":
2524
+ print("ERROR: changing to power-user requires its device and quota details; use interactive users manage", file=sys.stderr)
2525
+ return 1
2526
+
2527
+ result = {"user": username, "role": role}
2528
+ if flags.get("json"):
2529
+ print(json.dumps(result, separators=(",", ":")))
2530
+ else:
2531
+ print(f"Set {username} role to {role}.")
2532
+ return 0
2533
+
2534
+
2319
2535
  def cmd_hosts_token(argv: List[str]) -> int:
2320
2536
  from .host.host_auth_token import create_host_token
2321
2537
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.20
3
+ Version: 1.7.22
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -113,14 +113,36 @@ class ReservedTotalTests(unittest.TestCase):
113
113
  now = datetime.now()
114
114
  add = "INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES ('u', 1, ?, ?, '', '')"
115
115
  total = lambda: db.execute("SELECT seconds FROM ReservedTotal").fetchone()[0]
116
- db.execute(add, (now - timedelta(hours=3), now - timedelta(hours=2))) # 1 h, past
117
116
  db.execute(add, (now + timedelta(hours=1), now + timedelta(hours=3))) # 2 h, future
117
+ db.execute(add, (now + timedelta(hours=4), now + timedelta(hours=5))) # 1 h, future
118
118
  self.assertAlmostEqual(total(), 3 * 3600, delta=1)
119
+ # Let the 1 h one end; moving it back by UPDATE fires no trigger.
120
+ db.execute("UPDATE Reservations SET start_time = ?, end_time = ? WHERE start_time > ?",
121
+ (now - timedelta(hours=3), now - timedelta(hours=2), now + timedelta(hours=2)))
119
122
  db.execute("DELETE FROM Reservations WHERE end_time < ?", (now,)) # purge: no change
120
123
  self.assertAlmostEqual(total(), 3 * 3600, delta=1)
121
124
  db.execute("DELETE FROM Reservations") # cancel the future one: unused 2 h refunded
122
125
  self.assertAlmostEqual(total(), 3600, delta=2)
123
126
 
127
+ def test_a_booking_made_after_its_start_counts_only_from_then(self):
128
+ import sqlite3
129
+ from datetime import datetime, timedelta
130
+ from remoteRF_server.server import reservation
131
+
132
+ with tempfile.TemporaryDirectory() as tmp, \
133
+ mock.patch.object(reservation, "get_db_dir", return_value=Path(tmp)):
134
+ reservation.ReservationHandler()
135
+ db = sqlite3.connect(Path(tmp) / "reservations.db", isolation_level=None)
136
+ now = datetime.now()
137
+ add = "INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES ('u', 1, ?, ?, '', '')"
138
+ total = lambda: db.execute("SELECT seconds FROM ReservedTotal").fetchone()[0]
139
+ db.execute(add, (now - timedelta(minutes=15), now + timedelta(minutes=15)))
140
+ self.assertAlmostEqual(total(), 15 * 60, delta=2)
141
+ db.execute("DELETE FROM Reservations") # cancelled right away: nothing held
142
+ self.assertAlmostEqual(total(), 0, delta=2)
143
+ db.execute(add, (now - timedelta(hours=2), now - timedelta(hours=1))) # already over
144
+ self.assertAlmostEqual(total(), 0, delta=2)
145
+
124
146
  def test_a_partner_device_is_left_to_its_owner_even_with_the_old_triggers(self):
125
147
  import sqlite3
126
148
  from datetime import datetime, timedelta
@@ -255,15 +277,18 @@ class ReservationBucketsTests(unittest.TestCase):
255
277
 
256
278
  self.handler()
257
279
  db = self.connect()
258
- self.add(db, 1, "2026-11-02 02:00:00", "2026-11-02 03:00:00") # Mon 02:00 here = Sun 20:30 UTC
259
- self.add(db, 1, "2026-11-04 12:00:00", "2026-11-04 14:00:00") # Wed 12:00 here = Wed 06:30 UTC
260
- self.add(db, ALIAS_BASE, "2026-11-02 02:00:00", "2026-11-02 03:00:00")
280
+ self.add(db, 1, "2099-11-02 02:00:00", "2099-11-02 03:00:00") # Mon 02:00 here = Sun 20:30 UTC
281
+ self.add(db, 1, "2099-11-04 12:00:00", "2099-11-04 14:00:00") # Wed 12:00 here = Wed 06:30 UTC
282
+ self.add(db, ALIAS_BASE, "2099-11-02 02:00:00", "2099-11-02 03:00:00")
261
283
  expected = {"all": (2, 3 * 3600), "h20": (1, 3600), "d6": (1, 3600), "h06": (1, 2 * 3600), "d2": (1, 2 * 3600)}
262
284
  self.assertEqual(self.buckets(db, 1), expected)
263
285
  self.assertEqual(self.buckets(db, ALIAS_BASE), {})
264
- days = {"2026-11-01": (1, 3600), "2026-11-04": (1, 2 * 3600)} # the first starts the UTC day before
286
+ days = {"2099-11-01": (1, 3600), "2099-11-04": (1, 2 * 3600)} # the first starts the UTC day before
265
287
  self.assertEqual(self.days(db, 1), days)
266
288
  self.assertEqual(self.days(db, ALIAS_BASE), {})
289
+ # Let them end; moving them back by UPDATE fires no trigger.
290
+ db.execute("UPDATE Reservations SET start_time = datetime(start_time, '-100 years'), "
291
+ "end_time = datetime(end_time, '-100 years')")
267
292
  db.execute("DELETE FROM Reservations WHERE end_time < ?", (datetime.now(),)) # purge: no change
268
293
  self.assertEqual(self.buckets(db, 1), expected)
269
294
  self.assertEqual(self.days(db, 1), days)
@@ -281,10 +306,13 @@ class ReservationBucketsTests(unittest.TestCase):
281
306
  self.assertEqual(values(2), [(1, 2 * 3600)] * 4)
282
307
  db.execute("DELETE FROM Reservations WHERE device_id = 2") # cancel before start: all given back
283
308
  self.assertEqual(values(2), [(0, 0)] * 4)
309
+ for n, seconds in values(3): # booked an hour after its start: only the rest counts
310
+ self.assertEqual(n, 1)
311
+ self.assertAlmostEqual(seconds, 3600, delta=2)
284
312
  db.execute("DELETE FROM Reservations WHERE device_id = 3") # cancel after start: unused hour back
285
313
  for n, seconds in values(3):
286
314
  self.assertEqual(n, 1)
287
- self.assertAlmostEqual(seconds, 3600, delta=2)
315
+ self.assertAlmostEqual(seconds, 0, delta=2)
288
316
  for table in ("ReservationBuckets", "ReservationDays"):
289
317
  db.execute(f"UPDATE {table} SET reservations = 0, seconds = 0 WHERE device_id = 4")
290
318
  db.execute("DELETE FROM Reservations WHERE device_id = 4")
@@ -317,9 +345,30 @@ class ReservationBucketsTests(unittest.TestCase):
317
345
  time.tzset()
318
346
  self.assertEqual(self.buckets(db, 1), expected)
319
347
  self.assertEqual(self.days(db, 1), days)
320
- self.add(db, 1, "2026-11-02 02:20:00", "2026-11-02 02:50:00") # 20:50 UTC
348
+ self.add(db, 1, "2099-11-02 02:20:00", "2099-11-02 02:50:00") # 20:50 UTC
321
349
  self.assertEqual(self.buckets(db, 1)["h20"], (3, 7200))
322
- self.assertEqual(self.days(db, 1), {"2026-11-01": (3, 7200)})
350
+ self.assertEqual(self.days(db, 1), {"2026-11-01": (2, 5400), "2099-11-01": (1, 1800)})
351
+
352
+ def test_a_restart_replaces_the_old_insert_triggers(self):
353
+ from datetime import datetime, timedelta
354
+
355
+ self.handler()
356
+ db = self.connect()
357
+ # A lab from before: its insert triggers count the full length.
358
+ for name in ("reservation_buckets_ins", "reservation_days_ins"):
359
+ sql = db.execute("SELECT sql FROM sqlite_master WHERE name = ?", (name,)).fetchone()[0]
360
+ old = sql.replace("MAX(NEW.start_time, datetime('now', 'localtime'))", "NEW.start_time")
361
+ self.assertNotEqual(old, sql)
362
+ db.executescript(f"DROP TRIGGER {name}; {old};")
363
+ self.handler()
364
+ for name in ("reservation_buckets_ins", "reservation_days_ins", "reserved_total_ins"):
365
+ sql = db.execute("SELECT sql FROM sqlite_master WHERE name = ?", (name,)).fetchone()[0]
366
+ self.assertIn("MAX(NEW.start_time, datetime('now', 'localtime'))", sql)
367
+ now = datetime.now()
368
+ self.add(db, 5, now - timedelta(minutes=15), now + timedelta(minutes=15))
369
+ for n, seconds in [*self.buckets(db, 5).values(), *self.days(db, 5).values()]:
370
+ self.assertEqual(n, 1)
371
+ self.assertAlmostEqual(seconds, 15 * 60, delta=2)
323
372
 
324
373
  def test_days_seed_into_an_existing_lab_that_already_has_buckets(self):
325
374
  """A lab upgrading from 1.7.14 has ReservationBuckets but no ReservationDays yet."""