nds-mapviewer 2026.1.2.dev29__tar.gz → 2026.1.2.dev30__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 (28) hide show
  1. {nds_mapviewer-2026.1.2.dev29/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.2.dev30}/PKG-INFO +1 -1
  2. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30/src/nds_mapviewer.egg-info}/PKG-INFO +1 -1
  3. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/container_runtime.py +7 -0
  4. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/docker_client.py +47 -25
  5. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/viewer.py +2 -1
  6. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_docker_client.py +92 -0
  7. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/.gitignore +0 -0
  8. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/CHANGELOG.md +0 -0
  9. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/README.md +0 -0
  10. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/pyproject.toml +0 -0
  11. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/setup.cfg +0 -0
  12. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/setup.py +0 -0
  13. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
  14. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
  15. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
  16. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/requires.txt +0 -0
  17. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
  18. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/__init__.py +0 -0
  19. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/browser.py +0 -0
  20. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/cli.py +0 -0
  21. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/config.py +0 -0
  22. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/exceptions.py +0 -0
  23. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/tui.py +0 -0
  24. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/ui.py +0 -0
  25. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/conftest.py +0 -0
  26. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_cli.py +0 -0
  27. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_config.py +0 -0
  28. {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_viewer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.1.2.dev29
3
+ Version: 2026.1.2.dev30
4
4
  Summary: CLI to run the NDS MapViewer Docker container for visualizing map data (NDS.Live, GeoJSON, and more)
5
5
  Author-email: NDS Association <support@nds-association.org>
6
6
  License: Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.1.2.dev29
3
+ Version: 2026.1.2.dev30
4
4
  Summary: CLI to run the NDS MapViewer Docker container for visualizing map data (NDS.Live, GeoJSON, and more)
5
5
  Author-email: NDS Association <support@nds-association.org>
6
6
  License: Proprietary
@@ -63,6 +63,7 @@ def run(
63
63
  *args: str,
64
64
  check: bool = True,
65
65
  timeout: float | None = 30,
66
+ extra_env: dict[str, str] | None = None,
66
67
  ) -> subprocess.CompletedProcess[str]:
67
68
  """Run a container-runtime command, capturing stdout and stderr.
68
69
 
@@ -75,14 +76,20 @@ def run(
75
76
  Raise :class:`subprocess.CalledProcessError` on non-zero exit.
76
77
  timeout:
77
78
  Seconds before the process is killed (``None`` = no limit).
79
+ extra_env:
80
+ Additional environment variables merged into the current env.
78
81
  """
79
82
  cmd = [detect_runtime(), *args]
83
+ env = None
84
+ if extra_env:
85
+ env = {**os.environ, **extra_env}
80
86
  return subprocess.run(
81
87
  cmd,
82
88
  capture_output=True,
83
89
  text=True,
84
90
  check=check,
85
91
  timeout=timeout,
92
+ env=env,
86
93
  )
87
94
 
88
95
 
@@ -182,6 +182,9 @@ def get_image_tag(edition: str, version: str, arch: str | None = None) -> str:
182
182
  # Registry / auth helpers
183
183
  # ---------------------------------------------------------------------------
184
184
 
185
+ _MANIFEST_ENV = {"DOCKER_CLI_EXPERIMENTAL": "enabled"}
186
+
187
+
185
188
  def _verify_registry_access(edition: str) -> bool:
186
189
  """Verify actual registry access via a quick manifest inspect (3 s timeout)."""
187
190
  import concurrent.futures
@@ -190,7 +193,8 @@ def _verify_registry_access(edition: str) -> bool:
190
193
  try:
191
194
  get_docker_client()
192
195
  tag = get_image_tag(edition, "latest")
193
- rt.run("manifest", "inspect", tag, timeout=3)
196
+ rt.run("manifest", "inspect", tag, timeout=3,
197
+ extra_env=_MANIFEST_ENV)
194
198
  return True
195
199
  except Exception:
196
200
  return False
@@ -299,6 +303,36 @@ def image_exists(edition: str, version: str = "latest", arch: str | None = None)
299
303
  return False
300
304
 
301
305
 
306
+ def _parse_image_entry(img: dict) -> tuple[str, float]:
307
+ """Extract (tag, size_mb) from Docker or Podman image JSON.
308
+
309
+ Docker ``images --format json`` produces keys like
310
+ ``Tag``, ``Repository``, ``Size`` (human-readable string).
311
+
312
+ Podman ``images --format json`` produces keys like
313
+ ``names`` (list of full refs), ``size`` (int bytes).
314
+ """
315
+ # Docker format: {"Repository": "...", "Tag": "latest-arm64", "Size": "1.2GB"}
316
+ tag_str = img.get("Tag", "")
317
+
318
+ if not tag_str:
319
+ # Podman format: {"names": ["registry/image:tag"], "size": 123456}
320
+ names = img.get("names") or img.get("Names") or []
321
+ if names:
322
+ # names is a list of full references like "registry/image:tag"
323
+ ref = names[0] if isinstance(names, list) else names
324
+ if ":" in ref:
325
+ tag_str = ref.rsplit(":", 1)[-1]
326
+
327
+ size = img.get("Size") or img.get("size") or 0
328
+ if isinstance(size, str):
329
+ size_mb = round(rt.parse_size_string(size) / (1024 * 1024), 1)
330
+ else:
331
+ size_mb = round(size / (1024 * 1024), 1)
332
+
333
+ return tag_str, size_mb
334
+
335
+
302
336
  def list_local_image_versions(edition: str | None = None) -> list[dict]:
303
337
  """List locally available nds-mapviewer images.
304
338
 
@@ -318,38 +352,25 @@ def list_local_image_versions(edition: str | None = None) -> list[dict]:
318
352
  continue
319
353
  repo = f"{registry}/{IMAGE_NAME}"
320
354
  try:
321
- data = rt.run_json(
355
+ raw = rt.run(
322
356
  "images", "--format", "json",
323
357
  "--filter", f"reference={repo}",
324
358
  timeout=10,
325
359
  )
326
- # Some runtimes return a list, others return newline-delimited JSON
327
- if isinstance(data, dict):
328
- data = [data]
329
- except (json.JSONDecodeError, subprocess.CalledProcessError):
330
- # Try newline-delimited JSON fallback
331
- try:
332
- raw = rt.run(
333
- "images", "--format", "json",
334
- "--filter", f"reference={repo}",
335
- timeout=10,
336
- )
337
- data = [json.loads(line) for line in raw.stdout.strip().splitlines() if line.strip()]
338
- except Exception:
360
+ # Docker outputs newline-delimited JSON objects,
361
+ # Podman outputs a single JSON array
362
+ stdout = raw.stdout.strip()
363
+ if not stdout:
339
364
  continue
365
+ if stdout.startswith("["):
366
+ data = json.loads(stdout)
367
+ else:
368
+ data = [json.loads(line) for line in stdout.splitlines() if line.strip()]
340
369
  except Exception:
341
370
  continue
342
371
 
343
372
  for img in data:
344
- # Handle different output formats from docker/podman
345
- tag_str = img.get("Tag", "")
346
- repo_str = img.get("Repository", "")
347
- size = img.get("Size", "0")
348
- if isinstance(size, str):
349
- size_mb = round(rt.parse_size_string(size) / (1024 * 1024), 1)
350
- else:
351
- size_mb = round(size / (1024 * 1024), 1)
352
-
373
+ tag_str, size_mb = _parse_image_entry(img)
353
374
  if not tag_str or tag_str == "<none>":
354
375
  continue
355
376
  version_str = re.sub(r"-(amd64|arm64)$", "", tag_str)
@@ -426,7 +447,8 @@ def get_remote_digest(
426
447
  ensure_logged_in(edition)
427
448
  image_tag = get_image_tag(edition, version, arch)
428
449
  try:
429
- result = rt.run("manifest", "inspect", image_tag, timeout=15)
450
+ result = rt.run("manifest", "inspect", image_tag, timeout=15,
451
+ extra_env=_MANIFEST_ENV)
430
452
  # Parse manifest JSON to extract digest
431
453
  try:
432
454
  manifest = json.loads(result.stdout)
@@ -305,10 +305,11 @@ class MapViewer:
305
305
  f"Recent logs:\n{logs}"
306
306
  )
307
307
  else:
308
+ from . import container_runtime as _rt
308
309
  raise ConfigError(
309
310
  f"Container did not become ready within {timeout}s.\n"
310
311
  f"The MapViewer is still starting up. Try increasing the timeout or check:\n"
311
- f" docker logs {self._name}"
312
+ f" {_rt.detect_runtime()} logs {self._name}"
312
313
  )
313
314
 
314
315
  def stop(self, remove: bool = True) -> None:
@@ -1,5 +1,6 @@
1
1
  """Tests for container runtime operations (requires Docker or Podman)."""
2
2
 
3
+ import json
3
4
  import subprocess
4
5
 
5
6
  import pytest
@@ -314,3 +315,94 @@ class TestContainerInfo:
314
315
  assert info.name == "test"
315
316
  assert info.status == "running"
316
317
  assert info.host_port == 8080
318
+
319
+
320
+ class TestParseImageEntry:
321
+ """Tests for _parse_image_entry with Docker and Podman JSON formats."""
322
+
323
+ def test_docker_format(self):
324
+ """Docker image JSON uses Tag/Repository/Size keys."""
325
+ img = {
326
+ "Repository": "artifactory.nds-association.org/ndslive-dockerreg/nds-mapviewer",
327
+ "Tag": "latest-arm64",
328
+ "Size": "1.2GB",
329
+ }
330
+ tag, size_mb = dc._parse_image_entry(img)
331
+ assert tag == "latest-arm64"
332
+ assert size_mb > 0
333
+
334
+ def test_podman_format(self):
335
+ """Podman image JSON uses names (list) and size (int) keys."""
336
+ img = {
337
+ "names": [
338
+ "artifactory.nds-association.org/ndslive-dockerreg/nds-mapviewer:latest-arm64"
339
+ ],
340
+ "size": 512 * 1024 * 1024,
341
+ "digest": "sha256:abc123",
342
+ }
343
+ tag, size_mb = dc._parse_image_entry(img)
344
+ assert tag == "latest-arm64"
345
+ assert size_mb == 512.0
346
+
347
+ def test_podman_format_capitalized_names(self):
348
+ """Podman may also use capitalized Names key."""
349
+ img = {
350
+ "Names": [
351
+ "registry.example.com/image:v1.0-amd64"
352
+ ],
353
+ "size": 256 * 1024 * 1024,
354
+ }
355
+ tag, size_mb = dc._parse_image_entry(img)
356
+ assert tag == "v1.0-amd64"
357
+ assert size_mb == 256.0
358
+
359
+ def test_empty_entry(self):
360
+ """Empty dict returns empty tag."""
361
+ tag, size_mb = dc._parse_image_entry({})
362
+ assert tag == ""
363
+ assert size_mb == 0.0
364
+
365
+
366
+ class TestListLocalImagesFormats:
367
+ """Test list_local_image_versions with mocked Docker and Podman output."""
368
+
369
+ def test_docker_newline_delimited_json(self, docker_client, mocker):
370
+ """Docker outputs newline-delimited JSON objects."""
371
+ registry = dc.REGISTRIES["community"]
372
+ mock_result = mocker.MagicMock()
373
+ mock_result.stdout = (
374
+ f'{{"Repository":"{registry}/nds-mapviewer","Tag":"latest-arm64","Size":"1.2GB"}}\n'
375
+ f'{{"Repository":"{registry}/nds-mapviewer","Tag":"dev-arm64","Size":"1.1GB"}}\n'
376
+ )
377
+ mock_result.returncode = 0
378
+ mocker.patch.object(rt, "run", return_value=mock_result)
379
+
380
+ results = dc.list_local_image_versions("community")
381
+ assert len(results) == 2
382
+ assert results[0]["version"] == "latest"
383
+ assert results[1]["version"] == "dev"
384
+
385
+ def test_podman_json_array(self, docker_client, mocker):
386
+ """Podman outputs a single JSON array."""
387
+ registry = dc.REGISTRIES["community"]
388
+ mock_result = mocker.MagicMock()
389
+ mock_result.stdout = json.dumps([
390
+ {
391
+ "names": [f"{registry}/nds-mapviewer:latest-arm64"],
392
+ "size": 512 * 1024 * 1024,
393
+ "digest": "sha256:abc",
394
+ },
395
+ {
396
+ "names": [f"{registry}/nds-mapviewer:dev-arm64"],
397
+ "size": 400 * 1024 * 1024,
398
+ "digest": "sha256:def",
399
+ },
400
+ ])
401
+ mock_result.returncode = 0
402
+ mocker.patch.object(rt, "run", return_value=mock_result)
403
+
404
+ results = dc.list_local_image_versions("community")
405
+ assert len(results) == 2
406
+ assert results[0]["version"] == "latest"
407
+ assert results[0]["size_mb"] == 512.0
408
+ assert results[1]["version"] == "dev"