lager-cli 0.46.0__tar.gz → 0.46.2__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 (161) hide show
  1. {lager_cli-0.46.0/lager_cli.egg-info → lager_cli-0.46.2}/PKG-INFO +1 -1
  2. {lager_cli-0.46.0 → lager_cli-0.46.2}/__init__.py +1 -1
  3. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/nets.py +95 -3
  4. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/exec_.py +92 -1
  5. {lager_cli-0.46.0 → lager_cli-0.46.2/lager_cli.egg-info}/PKG-INFO +1 -1
  6. {lager_cli-0.46.0 → lager_cli-0.46.2}/LICENSE +0 -0
  7. {lager_cli-0.46.0 → lager_cli-0.46.2}/MANIFEST.in +0 -0
  8. {lager_cli-0.46.0 → lager_cli-0.46.2}/README.md +0 -0
  9. {lager_cli-0.46.0 → lager_cli-0.46.2}/__main__.py +0 -0
  10. {lager_cli-0.46.0 → lager_cli-0.46.2}/address_utils.py +0 -0
  11. {lager_cli-0.46.0 → lager_cli-0.46.2}/battery/__init__.py +0 -0
  12. {lager_cli-0.46.0 → lager_cli-0.46.2}/battery/battery_tui.py +0 -0
  13. {lager_cli-0.46.0 → lager_cli-0.46.2}/battery/websocket_client.py +0 -0
  14. {lager_cli-0.46.0 → lager_cli-0.46.2}/box_storage.py +0 -0
  15. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/__init__.py +0 -0
  16. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/__init__.py +0 -0
  17. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_device_identity.py +0 -0
  18. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_host_ops.py +0 -0
  19. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_mount_prep.py +0 -0
  20. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_pip_validation.py +0 -0
  21. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_shim_verbs.py +0 -0
  22. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/_ssh.py +0 -0
  23. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/box_group.py +0 -0
  24. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/boxes.py +0 -0
  25. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/config.py +0 -0
  26. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/diagnose.py +0 -0
  27. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/dut.py +0 -0
  28. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/hello.py +0 -0
  29. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/instruments.py +0 -0
  30. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/labjack_pins.py +0 -0
  31. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/lock.py +0 -0
  32. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/net_tui.py +0 -0
  33. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/ssh.py +0 -0
  34. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/box/ssh_setup.py +0 -0
  35. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/__init__.py +0 -0
  36. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/ble.py +0 -0
  37. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/blufi.py +0 -0
  38. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/i2c.py +0 -0
  39. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/router.py +0 -0
  40. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/spi.py +0 -0
  41. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/uart.py +0 -0
  42. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/usb.py +0 -0
  43. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/websocket_client.py +0 -0
  44. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/communication/wifi.py +0 -0
  45. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/__init__.py +0 -0
  46. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/arm.py +0 -0
  47. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/__init__.py +0 -0
  48. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/commands.py +0 -0
  49. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/net_cache.py +0 -0
  50. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/rtt_websocket_client.py +0 -0
  51. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/service_client.py +0 -0
  52. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/service_helper.py +0 -0
  53. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/debug/tunnel.py +0 -0
  54. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/devenv.py +0 -0
  55. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/development/python.py +0 -0
  56. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/__init__.py +0 -0
  57. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/adc.py +0 -0
  58. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/dac.py +0 -0
  59. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/energy.py +0 -0
  60. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/gpi.py +0 -0
  61. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/gpo.py +0 -0
  62. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/logic.py +0 -0
  63. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/scope.py +0 -0
  64. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/thermocouple.py +0 -0
  65. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/measurement/watt.py +0 -0
  66. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/__init__.py +0 -0
  67. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/battery.py +0 -0
  68. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/battery_model_csv.py +0 -0
  69. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/eload.py +0 -0
  70. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/solar.py +0 -0
  71. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/power/supply.py +0 -0
  72. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/__init__.py +0 -0
  73. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/_host_cli.py +0 -0
  74. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/binaries.py +0 -0
  75. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/defaults.py +0 -0
  76. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/install.py +0 -0
  77. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/install_wheel.py +0 -0
  78. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/login.py +0 -0
  79. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/logs.py +0 -0
  80. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/uninstall.py +0 -0
  81. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/update.py +0 -0
  82. {lager_cli-0.46.0 → lager_cli-0.46.2}/commands/utility/webcam.py +0 -0
  83. {lager_cli-0.46.0 → lager_cli-0.46.2}/config.py +0 -0
  84. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/__init__.py +0 -0
  85. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/ci_detection.py +0 -0
  86. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/constants.py +0 -0
  87. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/core.py +0 -0
  88. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/error_handlers.py +0 -0
  89. {lager_cli-0.46.0 → lager_cli-0.46.2}/context/session.py +0 -0
  90. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/__init__.py +0 -0
  91. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/group_usage.py +0 -0
  92. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/matchers.py +0 -0
  93. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/net_group.py +0 -0
  94. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/net_helpers.py +0 -0
  95. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/net_storage.py +0 -0
  96. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/param_types.py +0 -0
  97. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/ssh_utils.py +0 -0
  98. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/utils.py +0 -0
  99. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/version_skew.py +0 -0
  100. {lager_cli-0.46.0 → lager_cli-0.46.2}/core/ws_diagnose.py +0 -0
  101. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/__init__.py +0 -0
  102. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/__init__.py +0 -0
  103. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/configure_docker_dns.py +0 -0
  104. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/configure_docker_dns.sh +0 -0
  105. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
  106. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/setup_and_deploy_box.sh +0 -0
  107. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/scripts/setup_ssh_key.sh +0 -0
  108. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/security/__init__.py +0 -0
  109. {lager_cli-0.46.0 → lager_cli-0.46.2}/deployment/security/secure_box_firewall.sh +0 -0
  110. {lager_cli-0.46.0 → lager_cli-0.46.2}/errors.py +0 -0
  111. {lager_cli-0.46.0 → lager_cli-0.46.2}/exceptions.py +0 -0
  112. {lager_cli-0.46.0 → lager_cli-0.46.2}/gateway_auth.py +0 -0
  113. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/__init__.py +0 -0
  114. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/box_config.py +0 -0
  115. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/measurement/__init__.py +0 -0
  116. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/measurement/scope.py +0 -0
  117. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/measurement/scope_stream.py +0 -0
  118. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/power/__init__.py +0 -0
  119. {lager_cli-0.46.0 → lager_cli-0.46.2}/impl/power/enable_disable.py +0 -0
  120. {lager_cli-0.46.0 → lager_cli-0.46.2}/lager_cli.egg-info/SOURCES.txt +0 -0
  121. {lager_cli-0.46.0 → lager_cli-0.46.2}/lager_cli.egg-info/dependency_links.txt +0 -0
  122. {lager_cli-0.46.0 → lager_cli-0.46.2}/lager_cli.egg-info/entry_points.txt +0 -0
  123. {lager_cli-0.46.0 → lager_cli-0.46.2}/lager_cli.egg-info/requires.txt +0 -0
  124. {lager_cli-0.46.0 → lager_cli-0.46.2}/lager_cli.egg-info/top_level.txt +0 -0
  125. {lager_cli-0.46.0 → lager_cli-0.46.2}/main.py +0 -0
  126. {lager_cli-0.46.0 → lager_cli-0.46.2}/pyproject.toml +0 -0
  127. {lager_cli-0.46.0 → lager_cli-0.46.2}/safe_unpickle.py +0 -0
  128. {lager_cli-0.46.0 → lager_cli-0.46.2}/setup.cfg +0 -0
  129. {lager_cli-0.46.0 → lager_cli-0.46.2}/setup.py +0 -0
  130. {lager_cli-0.46.0 → lager_cli-0.46.2}/simple_hdlc.py +0 -0
  131. {lager_cli-0.46.0 → lager_cli-0.46.2}/sort_utils.py +0 -0
  132. {lager_cli-0.46.0 → lager_cli-0.46.2}/status.py +0 -0
  133. {lager_cli-0.46.0 → lager_cli-0.46.2}/supply/__init__.py +0 -0
  134. {lager_cli-0.46.0 → lager_cli-0.46.2}/supply/supply_tui.py +0 -0
  135. {lager_cli-0.46.0 → lager_cli-0.46.2}/supply/websocket_client.py +0 -0
  136. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/__init__.py +0 -0
  137. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/core/__init__.py +0 -0
  138. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/core/executor.py +0 -0
  139. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/main.py +0 -0
  140. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/__init__.py +0 -0
  141. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/completer.py +0 -0
  142. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/display.py +0 -0
  143. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/logo.py +0 -0
  144. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/repl.py +0 -0
  145. {lager_cli-0.46.0 → lager_cli-0.46.2}/terminal/ui/themes.py +0 -0
  146. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_box_lager_imports.py +0 -0
  147. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_box_storage.py +0 -0
  148. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_gateway_auth.py +0 -0
  149. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_gateway_callsites.py +0 -0
  150. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_host_cli.py +0 -0
  151. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_io_imports.py +0 -0
  152. {lager_cli-0.46.0 → lager_cli-0.46.2}/tests/test_update_gate.py +0 -0
  153. {lager_cli-0.46.0 → lager_cli-0.46.2}/update_check.py +0 -0
  154. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRC16.py +0 -0
  155. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRC16DNP.py +0 -0
  156. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRC16Kermit.py +0 -0
  157. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRC16SICK.py +0 -0
  158. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRC32.py +0 -0
  159. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/CRCCCITT.py +0 -0
  160. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/PyCRC/__init__.py +0 -0
  161. {lager_cli-0.46.0 → lager_cli-0.46.2}/vendor/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lager-cli
3
- Version: 0.46.0
3
+ Version: 0.46.2
4
4
  Summary: Lager CLI - Box and Docker connectivity
5
5
  Home-page: https://github.com/lagerdata/lager
6
6
  Author: Lager Data
@@ -7,4 +7,4 @@ Lager CLI
7
7
  A Command Line Interface for Lager Data
8
8
  """
9
9
 
10
- __version__ = '0.46.0'
10
+ __version__ = '0.46.2'
@@ -422,6 +422,33 @@ def canonical_instrument(instrument):
422
422
  return _INSTRUMENT_ALIASES.get(instrument, instrument)
423
423
 
424
424
 
425
+ # Pins whose rejection has a better answer than "pick another channel". On a
426
+ # U3 these four are the fixed high-voltage analog inputs, so a user reaching
427
+ # for gpio there wants an adc net on the SAME physical pins, under the name
428
+ # they carry in analog mode. The scanner stopped advertising them as gpio
429
+ # (box/lager/http_handlers/usb_scanner.py), and this is what turns the
430
+ # resulting rejection into a next step rather than a dead end. Raw DIO
431
+ # numbers are included because the U3 drivers accept either spelling.
432
+ _U3_HV_PINS = {"FIO0", "FIO1", "FIO2", "FIO3", "0", "1", "2", "3"}
433
+
434
+
435
+ def _channel_rejection_hint(instrument, role, channel):
436
+ """An extra line for channel rejections that have a specific remedy.
437
+
438
+ Returns None when the generic "here are the valid channels" line is the
439
+ whole story, which is the case for every instrument but the U3.
440
+ """
441
+ if (canonical_instrument(instrument) == "LabJack_U3"
442
+ and role == "gpio"
443
+ and str(channel).upper() in _U3_HV_PINS):
444
+ return (
445
+ "FIO0-FIO3 on a LabJack U3 are fixed high-voltage analog inputs "
446
+ "and are never digital I/O. Read them with an adc net on "
447
+ "AIN0-AIN3, the same physical pins."
448
+ )
449
+ return None
450
+
451
+
425
452
  # Chips that can run in exactly one mode at a time, across ALL roles. The
426
453
  # canonical case is the FT232H: one physical channel, hardware-multiplexed
427
454
  # between MPSSE (spi/i2c/gpio/debug) and async-serial (uart). Once the user
@@ -1325,6 +1352,18 @@ def add_cmd(ctx, name, role, channel, address, box, jlink_script, openocd_config
1325
1352
  f"The channel '{channel}' is not valid for role '{role}' on the instrument at {address}.",
1326
1353
  fg="red",
1327
1354
  )
1355
+ # Naming what IS valid, the way the role rejection below
1356
+ # does. Both lines stay on stdout with the one above them:
1357
+ # cli/setup.py pins only click>=8.1.2, and 8.2's CliRunner
1358
+ # stopped folding stderr into result.output, so moving them
1359
+ # would change what every existing CLI test can see.
1360
+ click.secho(
1361
+ f"Valid {role} channels for {instrument}: "
1362
+ f"{', '.join(str(ch) for ch in role_chans)}"
1363
+ )
1364
+ hint = _channel_rejection_hint(instrument, role, channel)
1365
+ if hint:
1366
+ click.secho(hint, fg="yellow")
1328
1367
  ctx.exit(1)
1329
1368
 
1330
1369
  # ─────────── unique net name (regardless of type) ────────────────
@@ -2040,7 +2079,13 @@ def create_batch_cmd(ctx: click.Context, json_file, box: str | None) -> None:
2040
2079
  # net never need the scan.
2041
2080
  _instrument_cache: list | None = None
2042
2081
 
2043
- def _get_instrument_from_address(address: str, fallback_instrument: str = "Unknown") -> str:
2082
+ def _get_scanned_device(address: str) -> dict | None:
2083
+ """The scanned record for *address*, or None when it is not present.
2084
+
2085
+ Shares the one lazy scan with _get_instrument_from_address. A batch
2086
+ run against a box that is down, or naming hardware that is unplugged,
2087
+ gets None here and keeps its pre-0.46.1 behavior.
2088
+ """
2044
2089
  nonlocal _instrument_cache
2045
2090
  if _instrument_cache is None:
2046
2091
  try:
@@ -2049,11 +2094,19 @@ def create_batch_cmd(ctx: click.Context, json_file, box: str | None) -> None:
2049
2094
  _instrument_cache = []
2050
2095
  for inst in _instrument_cache:
2051
2096
  if inst.get("address") == address:
2052
- return inst.get("name", "Unknown")
2053
- return fallback_instrument
2097
+ return inst
2098
+ return None
2099
+
2100
+ def _get_instrument_from_address(address: str, fallback_instrument: str = "Unknown") -> str:
2101
+ inst = _get_scanned_device(address)
2102
+ return inst.get("name", "Unknown") if inst else fallback_instrument
2054
2103
 
2055
2104
  # Validate and normalize each net in the batch
2056
2105
  normalized_nets = []
2106
+ # Channel rejections are collected, not raised in the loop: _save_nets_batch
2107
+ # is all-or-nothing, so telling the user about record 3 and hiding record 7
2108
+ # would cost them a second run to find the next one.
2109
+ channel_errors: list[str] = []
2057
2110
 
2058
2111
  for i, net_data in enumerate(nets_data):
2059
2112
  if not isinstance(net_data, dict):
@@ -2080,8 +2133,47 @@ def create_batch_cmd(ctx: click.Context, json_file, box: str | None) -> None:
2080
2133
  "pin": net_data["channel"],
2081
2134
  "instrument": instrument
2082
2135
  }
2136
+
2137
+ # Reject a channel the present hardware does not offer, the way
2138
+ # `nets add` does. Deliberately narrow:
2139
+ #
2140
+ # * only when the device is actually in the scan. This command is
2141
+ # built to provision a box whose instruments are absent or named
2142
+ # by bare IP, and _get_instrument_from_address already falls back
2143
+ # to "Unknown" for those. Rejecting what we cannot see would break
2144
+ # that, fleet-wide, for hardware unrelated to the U3.
2145
+ # * never for uart, whose channels are per-device tty paths rather
2146
+ # than the static list (see uart_channel_paths).
2147
+ #
2148
+ # What it does catch is the case this exists for: a hand-written
2149
+ # record naming a pin the instrument cannot drive, which used to be
2150
+ # saved without complaint and fail at first use.
2151
+ batch_role = normalized_net["role"]
2152
+ if batch_role != "uart":
2153
+ dev = _get_scanned_device(net_data["address"])
2154
+ role_chans = (dev.get("channels") or {}).get(batch_role) if dev else None
2155
+ if isinstance(role_chans, list) and role_chans:
2156
+ if str(net_data["channel"]) not in [str(ch) for ch in role_chans]:
2157
+ detail = (
2158
+ f"Net {i+1} ('{net_data['name']}'): channel "
2159
+ f"'{net_data['channel']}' is not valid for role "
2160
+ f"'{batch_role}' on {instrument}. Valid: "
2161
+ f"{', '.join(str(ch) for ch in role_chans)}"
2162
+ )
2163
+ hint = _channel_rejection_hint(
2164
+ instrument, batch_role, net_data["channel"])
2165
+ if hint:
2166
+ detail = f"{detail}\n {hint}"
2167
+ channel_errors.append(detail)
2168
+
2083
2169
  normalized_nets.append(normalized_net)
2084
2170
 
2171
+ if channel_errors:
2172
+ for detail in channel_errors:
2173
+ click.secho(detail, fg="red", err=True)
2174
+ click.secho("No nets were saved.", fg="red", err=True)
2175
+ ctx.exit(1)
2176
+
2085
2177
  # Use batch save for better performance
2086
2178
  _save_nets_batch(ctx, resolved_box, normalized_nets)
2087
2179
 
@@ -16,6 +16,7 @@ import platform
16
16
  from pathlib import Path
17
17
  import click
18
18
  from ...config import get_devenv_json, write_lager_json, LAGER_CONFIG_FILE_NAME, get_global_config_file_path, devenv_config_list, expand_devenv_path
19
+ from ...context.ci_detection import get_ci_environment, is_container_ci
19
20
  from ...core.param_types import EnvVarType
20
21
 
21
22
 
@@ -150,6 +151,81 @@ def _run_command_local(section, path, cmd_to_run, mount, extra_args, debug, inte
150
151
  return 1
151
152
 
152
153
 
154
+ # Config keys that only apply when we start a container. A .lager is shared between a
155
+ # developer's machine and CI, so carrying these is normal and not worth a warning on every
156
+ # CI run -- they are reported under --verbose instead.
157
+ _DOCKER_ONLY_CONFIG_KEYS = ('network', 'ports', 'platform', 'macaddr', 'hostname',
158
+ 'volumes', 'user', 'group')
159
+
160
+
161
+ def _warn_docker_only_options(section, mount, volumes, user, group, debug):
162
+ """
163
+ Name the options that cannot take effect when the command runs in place.
164
+
165
+ Flags passed on the command line are warned about unconditionally: the caller asked
166
+ for something this path cannot do. Config keys are quieter, for the reason above.
167
+ """
168
+ ignored = []
169
+ if mount:
170
+ ignored.append('--mount')
171
+ if volumes:
172
+ ignored.append('--volume')
173
+ if user:
174
+ ignored.append('--user')
175
+ if group:
176
+ ignored.append('--group')
177
+
178
+ if ignored:
179
+ click.secho(
180
+ f'Warning: {", ".join(ignored)} ignored: the command runs in the current '
181
+ f'container, so there is nothing to start.',
182
+ fg='yellow', err=True)
183
+
184
+ if debug:
185
+ from_config = [key for key in _DOCKER_ONLY_CONFIG_KEYS if section.get(key)]
186
+ if from_config:
187
+ click.echo(
188
+ f'Ignoring container-only .lager keys: {", ".join(from_config)}', err=True)
189
+
190
+
191
+ def _run_command_container(section, cmd_to_run, extra_args, debug, env):
192
+ """
193
+ Run a command in place, without starting a container.
194
+
195
+ A container-based CI job already runs inside the devenv image: the shell, the
196
+ toolchain and the checkout are all there. There is also no Docker inside that job
197
+ container, so starting one is not an option even if it were wanted.
198
+
199
+ Deliberately does not chdir. The Docker path sets `-w <mount_dir>` because it bind-mounts
200
+ the checkout to that path; here the checkout is wherever the CI system put it, and the
201
+ caller's working directory is already correct.
202
+ """
203
+ # `or`, not a .get default: a .lager may carry an explicit empty shell, and handing
204
+ # that to subprocess.run raises rather than reporting anything useful.
205
+ shell = section.get('shell') or '/bin/bash'
206
+ full_command = ' '.join((cmd_to_run, *extra_args)).strip()
207
+
208
+ child_env = os.environ.copy()
209
+ for env_var in (*devenv_config_list(section.get('environment')), *env):
210
+ name, _, value = str(env_var).partition('=')
211
+ child_env[name] = value
212
+
213
+ if debug:
214
+ click.echo(f'Command: {full_command}', err=True)
215
+ click.echo(f'Running in the current container ({shell})', err=True)
216
+
217
+ try:
218
+ proc = subprocess.run([shell, '-c', full_command], check=False, env=child_env)
219
+ return proc.returncode
220
+ except FileNotFoundError:
221
+ click.secho(f'Error: shell `{shell}` not found in this container', fg='red', err=True)
222
+ click.echo('Set DEVENV `shell` in .lager to a shell the image provides.', err=True)
223
+ return 1
224
+ except PermissionError:
225
+ click.secho(f'Error: permission denied running `{shell}`', fg='red', err=True)
226
+ return 1
227
+
228
+
153
229
  @click.command(name='exec', context_settings={"ignore_unknown_options": True},
154
230
  short_help="Run a saved command in a local docker container")
155
231
  @click.pass_context
@@ -192,6 +268,10 @@ def exec_(ctx, cmd_name, extra_args, command, save_as, warn, env, passenv, mount
192
268
 
193
269
  section = data['DEVENV']
194
270
 
271
+ # Captured before the defaulting below, which always resolves a uid/gid: the
272
+ # in-container path warns about what the caller actually asked for, not the default.
273
+ requested_user, requested_group = user, group
274
+
195
275
  # Precedence: CLI flag wins, then .lager config, then the current uid/gid.
196
276
  if user is None:
197
277
  user = section.get('user')
@@ -248,9 +328,20 @@ def exec_(ctx, cmd_name, extra_args, command, save_as, warn, env, passenv, mount
248
328
  ctx.exit(1)
249
329
  click.echo(f'Saved command: {save_as}')
250
330
 
331
+ debug = ctx.obj.debug or verbose
332
+
333
+ # Under container-based CI the job is already running inside the devenv image, so the
334
+ # command runs in place. Starting a container from in there would need a Docker the job
335
+ # container does not have. LAGER_CI_OVERRIDE forces this back to the Docker path.
336
+ if is_container_ci(get_ci_environment()):
337
+ _warn_docker_only_options(section, mount, volumes, requested_user, requested_group, debug)
338
+ # passenv needs no argument here: the child inherits our environment already.
339
+ returncode = _run_command_container(section, cmd_to_run, extra_args, debug, env)
340
+ ctx.exit(returncode)
341
+
251
342
  # Run the command locally in Docker
252
343
  returncode = _run_command_local(
253
344
  section, path, cmd_to_run, mount, extra_args,
254
- ctx.obj.debug or verbose, interactive, tty, user, group, env, passenv, volumes
345
+ debug, interactive, tty, user, group, env, passenv, volumes
255
346
  )
256
347
  ctx.exit(returncode)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lager-cli
3
- Version: 0.46.0
3
+ Version: 0.46.2
4
4
  Summary: Lager CLI - Box and Docker connectivity
5
5
  Home-page: https://github.com/lagerdata/lager
6
6
  Author: Lager Data
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes