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.
- {nds_mapviewer-2026.1.2.dev29/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.2.dev30}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30/src/nds_mapviewer.egg-info}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/container_runtime.py +7 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/docker_client.py +47 -25
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/viewer.py +2 -1
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_docker_client.py +92 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/.gitignore +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/CHANGELOG.md +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/README.md +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/pyproject.toml +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/setup.cfg +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/setup.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/browser.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/cli.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/config.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/exceptions.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/tui.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/conftest.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_cli.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_config.py +0 -0
- {nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/tests/test_viewer.py +0 -0
{nds_mapviewer-2026.1.2.dev29/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.2.dev30}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.1.2.
|
|
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
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30/src/nds_mapviewer.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.1.2.
|
|
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
|
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/docker_client.py
RENAMED
|
@@ -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
|
-
|
|
355
|
+
raw = rt.run(
|
|
322
356
|
"images", "--format", "json",
|
|
323
357
|
"--filter", f"reference={repo}",
|
|
324
358
|
timeout=10,
|
|
325
359
|
)
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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)
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/viewer.py
RENAMED
|
@@ -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"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/nds_mapviewer.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/__init__.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/browser.py
RENAMED
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/config.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.1.2.dev29 → nds_mapviewer-2026.1.2.dev30}/src/ndslive/mapviewer/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|