remoteRF-server 1.7.16__tar.gz → 1.7.17__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 (208) hide show
  1. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/PKG-INFO +1 -1
  2. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/pyproject.toml +1 -1
  3. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/commands.py +2 -2
  4. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/serverrf_cli.py +222 -4
  5. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  6. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_commands.py +108 -0
  7. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/LICENSE +0 -0
  8. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/README.md +0 -0
  9. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/setup.cfg +0 -0
  10. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/__init__.py +0 -0
  11. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/__init__.py +0 -0
  12. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  13. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  14. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  15. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  16. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  17. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  18. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  19. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  20. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  21. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  22. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  23. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  24. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/__init__.py +0 -0
  25. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  26. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  27. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  28. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/schema.py +0 -0
  29. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/idl/v2.py +0 -0
  30. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/__init__.py +0 -0
  31. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  32. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/api_token.py +0 -0
  33. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  34. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/db_location.py +0 -0
  35. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/list_string.py +0 -0
  36. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/panel.py +0 -0
  37. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  38. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/__init__.py +0 -0
  39. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  40. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
  41. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  42. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  43. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  44. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  45. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
  46. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/gps_diag.py +0 -0
  47. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  48. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  49. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  50. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
  51. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  52. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  53. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  54. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  55. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  56. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  57. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  58. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
  59. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  60. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  61. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  62. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/wifi/__init__.py +0 -0
  63. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/drivers/wifi/wifi_schema.py +0 -0
  64. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/host/__init__.py +0 -0
  65. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/host/host_auth_token.py +0 -0
  66. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/host/host_directory_store.py +0 -0
  67. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  68. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/remote_admin_shell.py +0 -0
  69. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/__init__.py +0 -0
  70. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/acc_perms.py +0 -0
  71. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/cert_provider.py +0 -0
  72. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/console.py +0 -0
  73. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/control.py +0 -0
  74. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  75. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  76. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/device_manager.py +0 -0
  77. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/diag.py +0 -0
  78. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/direct_path.py +0 -0
  79. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  80. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  81. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/__init__.py +0 -0
  82. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/admin.py +0 -0
  83. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/cli.py +0 -0
  84. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/cluster.py +0 -0
  85. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/contracts.py +0 -0
  86. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/destination.py +0 -0
  87. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/devices.py +0 -0
  88. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  89. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/global_home.py +0 -0
  90. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/home.py +0 -0
  91. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  92. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/identity.py +0 -0
  93. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  94. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/links.py +0 -0
  95. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/migrations.py +0 -0
  96. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/peers.py +0 -0
  97. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/permissions.py +0 -0
  98. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/principals.py +0 -0
  99. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/runtime.py +0 -0
  100. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/federation/trust.py +0 -0
  101. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/global_onboarding.py +0 -0
  102. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/grpc_server.py +0 -0
  103. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/host_direct.py +0 -0
  104. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  105. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/json_output.py +0 -0
  106. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/remote_admin.py +0 -0
  107. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
  108. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/reservation.py +0 -0
  109. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/rpc_manager.py +0 -0
  110. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/schema_registry.py +0 -0
  111. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/startup_check.py +0 -0
  112. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/user_group_cli.py +0 -0
  113. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/user_group_handler.py +0 -0
  114. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/server/v2_errors.py +0 -0
  115. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/tools/__init__.py +0 -0
  116. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/tools/gen_certs.py +0 -0
  117. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  118. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  119. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  120. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  121. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/requires.txt +0 -0
  122. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  123. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/rssi/__init__.py +0 -0
  124. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/rssi/cli.py +0 -0
  125. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/rssi/dongle.py +0 -0
  126. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/rssi/safety.py +0 -0
  127. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/src/rssi/scanner.py +0 -0
  128. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_account_hardening.py +0 -0
  129. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_account_names.py +0 -0
  130. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_account_removal.py +0 -0
  131. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_activity_log.py +0 -0
  132. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_availability.py +0 -0
  133. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_bind_host.py +0 -0
  134. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_cert_provider.py +0 -0
  135. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_client_sessions.py +0 -0
  136. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_cluster.py +0 -0
  137. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_console.py +0 -0
  138. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_contrib_drivers.py +0 -0
  139. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_default_rtl_sdr_schema.py +0 -0
  140. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_default_usrp_schema.py +0 -0
  141. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_default_wifi_schema.py +0 -0
  142. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_deployment_capabilities.py +0 -0
  143. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_detached_device_rpc.py +0 -0
  144. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_device_core_aliases.py +0 -0
  145. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_device_handles.py +0 -0
  146. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_device_init.py +0 -0
  147. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_devices_add_wizard.py +0 -0
  148. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_devices_list.py +0 -0
  149. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_diag.py +0 -0
  150. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_direct_fixed_port.py +0 -0
  151. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_direct_path_server.py +0 -0
  152. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_dynamic_v2.py +0 -0
  153. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_federated_direct_path.py +0 -0
  154. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_federated_dynamic_v2.py +0 -0
  155. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_federation_edges.py +0 -0
  156. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_first_run.py +0 -0
  157. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_d_contracts.py +0 -0
  158. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_e_federation.py +0 -0
  159. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_e_proto.py +0 -0
  160. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_f_enrollment.py +0 -0
  161. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_g1_logging.py +0 -0
  162. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_g_permissions.py +0 -0
  163. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_h_devices.py +0 -0
  164. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gate_i_direct_path.py +0 -0
  165. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_gen_certs.py +0 -0
  166. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_global_accounts_migration.py +0 -0
  167. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_global_home.py +0 -0
  168. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_global_onboarding.py +0 -0
  169. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_global_v1_disposal.py +0 -0
  170. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_hackrf_schema.py +0 -0
  171. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_home_device_plane.py +0 -0
  172. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_host_direct.py +0 -0
  173. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_host_directory_store.py +0 -0
  174. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_host_dynamic_v2.py +0 -0
  175. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_idl_schema.py +0 -0
  176. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_json_commands.py +0 -0
  177. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_lab_logins.py +0 -0
  178. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_panel.py +0 -0
  179. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_peer_transport.py +0 -0
  180. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_pluto_compat.py +0 -0
  181. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_pluto_poe.py +0 -0
  182. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_process_arg.py +0 -0
  183. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_proxy_lifecycle.py +0 -0
  184. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_remote_admin.py +0 -0
  185. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_reservation_cancel.py +0 -0
  186. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_reservation_handoff.py +0 -0
  187. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_reservation_policy_b.py +0 -0
  188. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_reservation_timestamps.py +0 -0
  189. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_rpc_manager_batch.py +0 -0
  190. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_rpc_manager_calln.py +0 -0
  191. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_rpc_manager_host_legacy.py +0 -0
  192. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_rpc_manager_rx_n.py +0 -0
  193. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_rtl_sdr_schema.py +0 -0
  194. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_schema_registry.py +0 -0
  195. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_soak_federation.py +0 -0
  196. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_startup_check.py +0 -0
  197. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_ti_mmwave_hardware.py +0 -0
  198. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_ti_mmwave_schema.py +0 -0
  199. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_user_group_cli.py +0 -0
  200. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  201. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_client_server_hardware.py +0 -0
  202. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_client_server_localhost.py +0 -0
  203. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  204. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_preload.py +0 -0
  205. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_profiles.py +0 -0
  206. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  207. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/tests/test_usrp_uhd410_hardware.py +0 -0
  208. {remoterf_server-1.7.16 → remoterf_server-1.7.17}/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.16
3
+ Version: 1.7.17
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.16"
7
+ version = "1.7.17"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -130,7 +130,7 @@ def _table() -> list[Command]:
130
130
  Command(("reservations purge",), "Remove ALL reservations", _daemon("cmd_reservations_purge"), "Reservations", where=D),
131
131
  # ---- groups ----
132
132
  Command(("groups list",), "List user groups", _shell("cmd_groups_list"), "Groups"),
133
- Command(("groups create",), "Create a user group (interactive)", _shell("cmd_groups_create"), "Groups"),
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
134
  Command(("groups edit",), "Edit a user group (interactive)", _shell("cmd_groups_edit"), "Groups"),
135
135
  Command(("groups remove",), "Delete a user group (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"),
@@ -138,7 +138,7 @@ def _table() -> list[Command]:
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
- Command(("codes create",), "Create enrollment codes for a group or a cluster (interactive)", _shell("cmd_codes_create"), "Enrollment codes"),
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
142
  Command(("codes remove",), "Delete an enrollment code (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 ----
@@ -1448,6 +1448,66 @@ def _yes(argv: List[str]) -> bool:
1448
1448
  return "-y" in argv or "--yes" in argv
1449
1449
 
1450
1450
 
1451
+ def _parse_create_args(argv, *, value_flags, bool_flags):
1452
+ """Small strict parser for the two forced-command-safe create forms."""
1453
+ positional, values = [], {}
1454
+ i = 0
1455
+ while i < len(argv):
1456
+ token = argv[i]
1457
+ if not token.startswith("--"):
1458
+ positional.append(token)
1459
+ i += 1
1460
+ continue
1461
+ option, equals, inline = token[2:].partition("=")
1462
+ if option in bool_flags:
1463
+ if equals:
1464
+ return None, f"--{option} does not take a value"
1465
+ values[option] = True
1466
+ i += 1
1467
+ continue
1468
+ if option not in value_flags:
1469
+ return None, f"unknown option --{option}"
1470
+ if equals:
1471
+ value = inline
1472
+ elif i + 1 < len(argv) and not argv[i + 1].startswith("--"):
1473
+ i += 1
1474
+ value = argv[i]
1475
+ else:
1476
+ return None, f"--{option} requires a value"
1477
+ values[option] = value
1478
+ i += 1
1479
+ return (positional, values), None
1480
+
1481
+
1482
+ def _bounded_int(value, option, *, minimum, maximum=None):
1483
+ try:
1484
+ number = int(value)
1485
+ except (TypeError, ValueError):
1486
+ raise ValueError(f"{option} must be an integer") from None
1487
+ if number < minimum:
1488
+ raise ValueError(f"{option} must be >= {minimum}")
1489
+ if maximum is not None and number > maximum:
1490
+ raise ValueError(f"{option} must be <= {maximum}")
1491
+ return number
1492
+
1493
+
1494
+ def _comma_ints(value, *, required=False):
1495
+ if value is None:
1496
+ if required:
1497
+ raise ValueError("--devices is required")
1498
+ return []
1499
+ parts = [part for part in str(value).replace(",", " ").split() if part]
1500
+ if not parts:
1501
+ raise ValueError("--devices requires one or more device ids")
1502
+ try:
1503
+ devices = [int(part) for part in parts]
1504
+ except ValueError:
1505
+ raise ValueError("--devices must be comma-separated integer ids") from None
1506
+ if any(device < 0 for device in devices):
1507
+ raise ValueError("--devices ids must be >= 0")
1508
+ return list(dict.fromkeys(devices))
1509
+
1510
+
1451
1511
  def _ask(prompt: str, default: str = "") -> str:
1452
1512
  try:
1453
1513
  value = input(f" {prompt}{f' [{default}]' if default else ''}: ").strip()
@@ -2051,9 +2111,96 @@ def cmd_groups_list(argv):
2051
2111
 
2052
2112
 
2053
2113
  def cmd_groups_create(argv):
2054
- from remoteRF_server.server.user_group_cli import make_user_group_local
2114
+ from remoteRF_server.server import user_group_cli as groups
2055
2115
 
2056
- make_user_group_local(_prompt_session())
2116
+ if not _positional(argv) or (sys.stdin.isatty() and not any(a.startswith("-") for a in argv)):
2117
+ groups.make_user_group_local(_prompt_session())
2118
+ return 0
2119
+ if not _yes(argv):
2120
+ print("ERROR: non-interactive group creation requires --yes", file=sys.stderr)
2121
+ return 1
2122
+
2123
+ parsed, error = _parse_create_args(
2124
+ argv,
2125
+ value_flags={"devices", "max-reservations", "max-time", "horizon", "lifetime", "share"},
2126
+ bool_flags={"yes", "json", "lan-only", "public"},
2127
+ )
2128
+ if error:
2129
+ print(f"ERROR: {error}", file=sys.stderr)
2130
+ return 1
2131
+ pos, flags = parsed
2132
+ if len(pos) != 1:
2133
+ print("ERROR: groups create requires exactly one group name", file=sys.stderr)
2134
+ return 1
2135
+ name = pos[0].strip()
2136
+ existing = groups.user_group_handler.get_all_user_groups(include_users=False) or []
2137
+ if any(g.get("group_name") == name for g in existing):
2138
+ print(f"ERROR: group {name!r} already exists", file=sys.stderr)
2139
+ return 1
2140
+
2141
+ try:
2142
+ devices = _comma_ints(flags.get("devices"), required=True)
2143
+ max_reservations = _bounded_int(flags.get("max-reservations", "3"), "--max-reservations", minimum=1)
2144
+ max_time = _bounded_int(flags.get("max-time", "1800"), "--max-time", minimum=1)
2145
+ horizon = _bounded_int(flags.get("horizon", str(7 * 86400)), "--horizon", minimum=0)
2146
+ lifetime = _bounded_int(flags.get("lifetime", "0"), "--lifetime", minimum=0)
2147
+ except ValueError as exc:
2148
+ print(f"ERROR: {exc}", file=sys.stderr)
2149
+ return 1
2150
+ lan_only = bool(flags.get("lan-only"))
2151
+ public = bool(flags.get("public"))
2152
+ shares = groups._names(str(flags.get("share", "none")))
2153
+ if lan_only and (public or shares):
2154
+ print("ERROR: a LAN-only group cannot be public or shared with partner clusters", file=sys.stderr)
2155
+ return 1
2156
+
2157
+ group_id = groups.user_group_handler.create_user_group(
2158
+ group_name=name,
2159
+ devices_whitelist=devices,
2160
+ max_reservation_time_sec=max_time,
2161
+ max_reservations=max_reservations,
2162
+ lifetime_sec=lifetime,
2163
+ lan_only=lan_only,
2164
+ max_horizon_sec=horizon,
2165
+ )
2166
+ if group_id is None:
2167
+ now = groups.user_group_handler.get_all_user_groups(include_users=False) or []
2168
+ if any(g.get("group_name") == name for g in now):
2169
+ print(f"ERROR: group {name!r} already exists", file=sys.stderr)
2170
+ else:
2171
+ print(f"ERROR: failed to create group {name!r}", file=sys.stderr)
2172
+ return 1
2173
+
2174
+ shared_with = []
2175
+ if public or shares:
2176
+ from remoteRF_server.server.federation import cluster
2177
+
2178
+ for cluster_name in (["global"] if public else []) + shares:
2179
+ try:
2180
+ cluster.attach_group(cluster_name, str(group_id))
2181
+ shared_with.append(cluster_name)
2182
+ except (cluster.ClusterError, RuntimeError, OSError) as exc:
2183
+ print(f"WARNING: group was not shared with {cluster_name}: {exc}", file=sys.stderr)
2184
+
2185
+ if flags.get("json"):
2186
+ created_group = next(
2187
+ (g for g in groups.user_group_handler.get_all_user_groups(include_users=False) or []
2188
+ if int(g.get("group_id", -1)) == int(group_id)),
2189
+ {},
2190
+ )
2191
+ print(json.dumps({
2192
+ "group_id": int(group_id), "group_name": name,
2193
+ "devices_whitelist": created_group.get("devices_whitelist", devices),
2194
+ "max_reservations": created_group.get("max_reservations", max_reservations),
2195
+ "max_reservation_time_sec": created_group.get("max_reservation_time_sec", max_time),
2196
+ "max_horizon_sec": created_group.get("max_horizon_sec", horizon),
2197
+ "lifetime_sec": created_group.get("lifetime_sec", lifetime),
2198
+ "lan_only": created_group.get("lan_only", lan_only),
2199
+ "public": "global" in shared_with, "shared_with": shared_with,
2200
+ }, separators=(",", ":")))
2201
+ else:
2202
+ print(f"Created group {name} (id {group_id})")
2203
+ return 0
2057
2204
 
2058
2205
 
2059
2206
  def cmd_groups_edit(argv):
@@ -2081,9 +2228,80 @@ def cmd_codes_list(argv):
2081
2228
 
2082
2229
 
2083
2230
  def cmd_codes_create(argv):
2084
- from remoteRF_server.server.user_group_cli import make_enrollment_codes_local
2231
+ from remoteRF_server.server import user_group_cli as groups
2232
+
2233
+ if not _positional(argv) or (sys.stdin.isatty() and not any(a.startswith("-") for a in argv)):
2234
+ groups.make_enrollment_codes_local(_prompt_session())
2235
+ return 0
2236
+ if not _yes(argv):
2237
+ print("ERROR: non-interactive enrollment-code creation requires --yes", file=sys.stderr)
2238
+ return 1
2239
+
2240
+ parsed, error = _parse_create_args(
2241
+ argv, value_flags={"count", "length", "uses", "duration"},
2242
+ bool_flags={"yes", "json"},
2243
+ )
2244
+ if error:
2245
+ print(f"ERROR: {error}", file=sys.stderr)
2246
+ return 1
2247
+ pos, flags = parsed
2248
+ if len(pos) != 1:
2249
+ print("ERROR: codes create requires exactly one group name, group id, or 'none'", file=sys.stderr)
2250
+ return 1
2085
2251
 
2086
- make_enrollment_codes_local(_prompt_session())
2252
+ ref = pos[0].strip()
2253
+ all_groups = groups.user_group_handler.get_all_user_groups(include_users=False) or []
2254
+ by_id = {int(g["group_id"]): g for g in all_groups}
2255
+ by_name = {str(g["group_name"]): g for g in all_groups}
2256
+ if ref.lower() == "none":
2257
+ group_id = -1
2258
+ else:
2259
+ selected = by_id.get(int(ref)) if ref.isdigit() else by_name.get(ref)
2260
+ if selected is None:
2261
+ print(f"ERROR: unknown group {ref!r}", file=sys.stderr)
2262
+ return 1
2263
+ group_id = int(selected["group_id"])
2264
+
2265
+ try:
2266
+ if "count" not in flags:
2267
+ raise ValueError("--count is required")
2268
+ count = _bounded_int(flags["count"], "--count", minimum=1, maximum=500)
2269
+ length = _bounded_int(flags.get("length", "10"), "--length", minimum=4)
2270
+ uses = _bounded_int(flags.get("uses", "1"), "--uses", minimum=1)
2271
+ duration = _bounded_int(flags.get("duration", "3600"), "--duration", minimum=0)
2272
+ except ValueError as exc:
2273
+ print(f"ERROR: {exc}", file=sys.stderr)
2274
+ return 1
2275
+
2276
+ code_names = groups.user_group_handler.generate_random_enrollment_code_names(
2277
+ count=count, char_count=length,
2278
+ ) or []
2279
+ if len(code_names) != count:
2280
+ print(f"ERROR: generated only {len(code_names)} of {count} enrollment codes", file=sys.stderr)
2281
+ return 1
2282
+ result = groups.user_group_handler.create_enrollment_codes(
2283
+ code_names=code_names, group_id=group_id, uses=uses, duration_sec=duration,
2284
+ )
2285
+ created = [row for row in groups.user_group_handler.get_all_enrollment_codes() or []
2286
+ if row.get("code_name") in code_names]
2287
+ if len(created) != count:
2288
+ print(f"ERROR: created only {len(created)} of {count} enrollment codes{f': {result}' if result else ''}", file=sys.stderr)
2289
+ return 1
2290
+ rows = groups._invites(created)
2291
+ if flags.get("json"):
2292
+ print(json.dumps({
2293
+ "group_id": group_id, "uses": uses, "duration_sec": duration,
2294
+ "invites": [row["invite"] for row in rows],
2295
+ }, separators=(",", ":")))
2296
+ return 0
2297
+
2298
+ print(f"\n{result}")
2299
+ if rows[0]["invite"] == rows[0]["code_name"]:
2300
+ print(f"Hand these out as is; users connect first with: remoterf -c -a {groups.lan_endpoint() or '<lan address>'}")
2301
+ else:
2302
+ print("Hand these out as is; users enter one at: remoterf --register")
2303
+ groups.print_table("Enrollment codes", rows, ["invite"])
2304
+ return 0
2087
2305
 
2088
2306
 
2089
2307
  def cmd_codes_remove(argv):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.16
3
+ Version: 1.7.17
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -2,10 +2,12 @@
2
2
 
3
3
  import contextlib
4
4
  import io
5
+ import json
5
6
  import os
6
7
  import tempfile
7
8
  import unittest
8
9
  from pathlib import Path
10
+ from types import SimpleNamespace
9
11
  from unittest import mock
10
12
 
11
13
  import yaml
@@ -158,3 +160,109 @@ class TwoDoorsTests(unittest.TestCase):
158
160
  self.assertEqual(dispatch(["global", "clusters"], in_daemon=True), 1)
159
161
  self.assertIn("Not configured", out.getvalue())
160
162
  self.assertIn("not enabled", out.getvalue())
163
+
164
+
165
+ class NonInteractiveGroupCommandsTests(unittest.TestCase):
166
+ def setUp(self):
167
+ self.tmp = tempfile.TemporaryDirectory()
168
+ self.addCleanup(self.tmp.cleanup)
169
+ self.cfg = Path(self.tmp.name) / "remoterf"
170
+ db = self.cfg / "db"
171
+ db.mkdir(parents=True)
172
+ env = mock.patch.dict(os.environ, {
173
+ "REMOTERF_CONFIG_DIR": str(self.cfg),
174
+ "XDG_CONFIG_HOME": self.tmp.name,
175
+ "XDG_DATA_HOME": str(Path(self.tmp.name) / "data"),
176
+ })
177
+ env.start()
178
+ self.addCleanup(env.stop)
179
+
180
+ from remoteRF_server.server import user_group_cli
181
+ from remoteRF_server.server import user_group_handler as handler_module
182
+
183
+ with mock.patch.object(handler_module, "get_db_dir", return_value=db):
184
+ self.handler = handler_module.UserGroupHandler()
185
+ handler = mock.patch.object(user_group_cli, "user_group_handler", self.handler)
186
+ handler.start()
187
+ self.addCleanup(handler.stop)
188
+
189
+ def run_shell(self, argv):
190
+ out = io.StringIO()
191
+ from remoteRF_server import serverrf_cli
192
+
193
+ stdin = SimpleNamespace(isatty=lambda: False)
194
+ with mock.patch.object(serverrf_cli.sys, "stdin", stdin), \
195
+ contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
196
+ rc = dispatch(argv, in_daemon=False)
197
+ return rc, out.getvalue()
198
+
199
+ def create_group(self):
200
+ return self.run_shell([
201
+ "groups", "create", "ee123", "--devices", "7,3,7",
202
+ "--max-reservations", "4", "--max-time", "900",
203
+ "--horizon", "172800", "--lifetime", "86400",
204
+ "--lan-only", "--yes", "--json",
205
+ ])
206
+
207
+ def test_group_create_from_flags_and_duplicate_is_refused(self):
208
+ rc, output = self.create_group()
209
+ self.assertEqual(rc, 0, output)
210
+ value = json.loads(output)
211
+ self.assertEqual(value["group_name"], "ee123")
212
+ self.assertEqual(value["devices_whitelist"], [3, 7])
213
+ group = self.handler.get_all_user_groups(include_users=False)[0]
214
+ self.assertEqual(group["devices_whitelist"], [3, 7])
215
+ self.assertEqual((group["max_reservations"], group["max_reservation_time_sec"]), (4, 900))
216
+ self.assertEqual((group["max_horizon_sec"], group["lifetime_sec"], group["lan_only"]),
217
+ (172800, 86400, True))
218
+
219
+ rc, output = self.run_shell([
220
+ "groups", "create", "ee123", "--devices", "3", "--yes",
221
+ ])
222
+ self.assertEqual(rc, 1)
223
+ self.assertEqual(output, "ERROR: group 'ee123' already exists\n")
224
+
225
+ def test_codes_create_json_honours_group_uses_and_duration(self):
226
+ rc, output = self.create_group()
227
+ self.assertEqual(rc, 0, output)
228
+ group_id = json.loads(output)["group_id"]
229
+
230
+ rc, output = self.run_shell([
231
+ "codes", "create", "ee123", "--count", "3", "--uses", "5",
232
+ "--duration", "7200", "--yes", "--json",
233
+ ])
234
+ self.assertEqual(rc, 0, output)
235
+ value = json.loads(output)
236
+ self.assertEqual(value["group_id"], group_id)
237
+ self.assertEqual((value["uses"], value["duration_sec"]), (5, 7200))
238
+ self.assertEqual(len(value["invites"]), 3)
239
+ rows = self.handler.get_all_enrollment_codes()
240
+ self.assertEqual({row["group_id"] for row in rows}, {group_id})
241
+ self.assertEqual({row["uses"] for row in rows}, {5})
242
+ self.assertEqual({row["duration_sec"] for row in rows}, {7200})
243
+ self.assertEqual(set(value["invites"]), {row["code_name"] for row in rows})
244
+
245
+ def test_unknown_group_and_missing_yes_are_refused(self):
246
+ rc, output = self.run_shell([
247
+ "codes", "create", "missing", "--count", "3", "--yes",
248
+ ])
249
+ self.assertEqual(rc, 1)
250
+ self.assertEqual(output, "ERROR: unknown group 'missing'\n")
251
+
252
+ rc, output = self.run_shell([
253
+ "groups", "create", "ee123", "--devices", "3",
254
+ ])
255
+ self.assertEqual(rc, 1)
256
+ self.assertEqual(output, "ERROR: non-interactive group creation requires --yes\n")
257
+
258
+ rc, output = self.run_shell([
259
+ "codes", "create", "none", "--count", "1",
260
+ ])
261
+ self.assertEqual(rc, 1)
262
+ self.assertEqual(output, "ERROR: non-interactive enrollment-code creation requires --yes\n")
263
+
264
+ def test_help_lists_flag_forms(self):
265
+ groups_help = "\n".join(commands.help_text(in_daemon=False, section="Groups"))
266
+ codes_help = "\n".join(commands.help_text(in_daemon=False, section="Enrollment codes"))
267
+ self.assertIn("groups create <name> --devices <ids>", groups_help)
268
+ self.assertIn("codes create <group name|id|none> --count N", codes_help)