nds-mapviewer 2026.1.3.dev45__tar.gz → 2026.1.3.dev47__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.3.dev45/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.3.dev47}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47/src/nds_mapviewer.egg-info}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/container_runtime.py +41 -9
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/docker_client.py +39 -12
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/exceptions.py +11 -3
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/tests/test_docker_client.py +228 -3
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/.gitignore +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/CHANGELOG.md +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/README.md +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/pyproject.toml +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/setup.cfg +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/setup.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/browser.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/cli.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/config.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/tui.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/viewer.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/tests/conftest.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/tests/test_cli.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/tests/test_config.py +0 -0
- {nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/tests/test_viewer.py +0 -0
{nds_mapviewer-2026.1.3.dev45/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.3.dev47}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.1.3.
|
|
3
|
+
Version: 2026.1.3.dev47
|
|
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.3.dev45 → nds_mapviewer-2026.1.3.dev47/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.3.
|
|
3
|
+
Version: 2026.1.3.dev47
|
|
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
|
|
@@ -42,18 +42,44 @@ def _runtime_is_responsive(runtime: str) -> bool:
|
|
|
42
42
|
return False
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _saved_runtime_preference() -> str | None:
|
|
46
|
+
"""Return the ``runtime_name`` saved by ``ndslive-setup``, if any.
|
|
47
|
+
|
|
48
|
+
``ndslive-setup`` writes ``~/.nds/setup.json`` after a successful wizard
|
|
49
|
+
run. When the user has explicitly configured a runtime there, we honour
|
|
50
|
+
that choice ahead of environment-based detection so the two tools stay
|
|
51
|
+
in sync.
|
|
52
|
+
"""
|
|
53
|
+
config_path = Path.home() / ".nds" / "setup.json"
|
|
54
|
+
if not config_path.exists():
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
with open(config_path) as f:
|
|
58
|
+
value = json.load(f).get("runtime_name")
|
|
59
|
+
except (json.JSONDecodeError, OSError):
|
|
60
|
+
return None
|
|
61
|
+
if value in ("docker", "podman"):
|
|
62
|
+
return value
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
45
66
|
@functools.lru_cache(maxsize=1)
|
|
46
67
|
def detect_runtime() -> str:
|
|
47
68
|
"""Return the name of the container runtime to use.
|
|
48
69
|
|
|
49
70
|
Resolution order:
|
|
50
71
|
|
|
51
|
-
1. ``
|
|
52
|
-
2. ``docker``
|
|
53
|
-
3. ``
|
|
72
|
+
1. Saved ``runtime_name`` in ``~/.nds/setup.json`` (written by ``ndslive-setup``).
|
|
73
|
+
2. ``CONTAINER_RUNTIME`` environment variable (``docker`` or ``podman``).
|
|
74
|
+
3. ``docker`` on ``$PATH``.
|
|
75
|
+
4. ``podman`` on ``$PATH``.
|
|
54
76
|
|
|
55
77
|
Raises :class:`FileNotFoundError` when no runtime is found.
|
|
56
78
|
"""
|
|
79
|
+
saved = _saved_runtime_preference()
|
|
80
|
+
if saved and _runtime_exists(saved):
|
|
81
|
+
return saved
|
|
82
|
+
|
|
57
83
|
env = os.environ.get("CONTAINER_RUNTIME", "").strip().lower()
|
|
58
84
|
if env in ("docker", "podman"):
|
|
59
85
|
if _runtime_exists(env):
|
|
@@ -74,7 +100,8 @@ def detect_runtime() -> str:
|
|
|
74
100
|
"No container runtime found.\n\n"
|
|
75
101
|
"Please install Docker or Podman:\n"
|
|
76
102
|
" Docker: https://docs.docker.com/get-docker/\n"
|
|
77
|
-
" Podman: https://podman.io/getting-started/installation"
|
|
103
|
+
" Podman: https://podman.io/getting-started/installation\n\n"
|
|
104
|
+
"Tip: run 'ndslive-setup doctor' for a guided diagnosis."
|
|
78
105
|
)
|
|
79
106
|
|
|
80
107
|
|
|
@@ -211,9 +238,13 @@ def get_auth_config_paths() -> list[Path]:
|
|
|
211
238
|
``~/.docker/config.json``
|
|
212
239
|
|
|
213
240
|
Podman (checked in order):
|
|
214
|
-
``$XDG_RUNTIME_DIR/containers/auth.json``
|
|
215
|
-
``~/.config/containers/auth.json``
|
|
216
|
-
|
|
241
|
+
``$XDG_RUNTIME_DIR/containers/auth.json`` (Linux session runtime dir)
|
|
242
|
+
``~/.config/containers/auth.json`` (Linux / macOS default)
|
|
243
|
+
``$APPDATA/containers/auth.json`` (Windows ``podman machine``)
|
|
244
|
+
|
|
245
|
+
Intentionally excludes ``~/.docker/config.json`` for podman: on macOS
|
|
246
|
+
Docker Desktop's ``credsStore`` helper runs on the host and podman
|
|
247
|
+
cannot invoke it, so a Docker login must not imply a Podman login.
|
|
217
248
|
"""
|
|
218
249
|
rt = detect_runtime()
|
|
219
250
|
paths: list[Path] = []
|
|
@@ -223,8 +254,9 @@ def get_auth_config_paths() -> list[Path]:
|
|
|
223
254
|
if xdg:
|
|
224
255
|
paths.append(Path(xdg) / "containers" / "auth.json")
|
|
225
256
|
paths.append(Path.home() / ".config" / "containers" / "auth.json")
|
|
226
|
-
|
|
227
|
-
|
|
257
|
+
appdata = os.environ.get("APPDATA")
|
|
258
|
+
if appdata:
|
|
259
|
+
paths.append(Path(appdata) / "containers" / "auth.json")
|
|
228
260
|
else:
|
|
229
261
|
paths.append(Path.home() / ".docker" / "config.json")
|
|
230
262
|
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/docker_client.py
RENAMED
|
@@ -222,21 +222,48 @@ def is_logged_in(edition: str) -> bool:
|
|
|
222
222
|
except (json.JSONDecodeError, OSError):
|
|
223
223
|
continue
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
for key in auths:
|
|
227
|
-
if registry_host in key or registry in key:
|
|
228
|
-
auth_data = auths[key]
|
|
229
|
-
if auth_data.get("auth") or auth_data.get("identitytoken"):
|
|
230
|
-
return True
|
|
231
|
-
|
|
232
|
-
cred_helpers = config.get("credHelpers", {})
|
|
233
|
-
if any(registry_host in k for k in cred_helpers):
|
|
225
|
+
if _auth_config_contains(config, registry, registry_host):
|
|
234
226
|
return True
|
|
235
227
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _auth_config_contains(
|
|
232
|
+
config: dict, registry: str, registry_host: str,
|
|
233
|
+
) -> bool:
|
|
234
|
+
"""Return True if ``config`` has credentials for ``registry``.
|
|
235
|
+
|
|
236
|
+
A registry counts as present when:
|
|
237
|
+
|
|
238
|
+
* ``auths[key]`` has an ``auth`` or ``identitytoken`` value, or
|
|
239
|
+
* ``auths`` has an entry for ``key`` and the file declares a
|
|
240
|
+
``credsStore`` (helper keeps the actual secret), or
|
|
241
|
+
* ``key`` appears in ``credHelpers`` (host or full registry path).
|
|
242
|
+
|
|
243
|
+
``credsStore`` alone (no matching ``auths`` entry) is not enough —
|
|
244
|
+
it only says "some creds live in the helper", not that any exist
|
|
245
|
+
for this registry.
|
|
246
|
+
"""
|
|
247
|
+
auths = config.get("auths", {}) or {}
|
|
248
|
+
creds_store = config.get("credsStore")
|
|
249
|
+
candidate_keys = (
|
|
250
|
+
registry_host,
|
|
251
|
+
f"https://{registry_host}",
|
|
252
|
+
registry,
|
|
253
|
+
f"https://{registry}",
|
|
254
|
+
)
|
|
255
|
+
for key in candidate_keys:
|
|
256
|
+
if key not in auths:
|
|
239
257
|
continue
|
|
258
|
+
entry = auths.get(key) or {}
|
|
259
|
+
if entry.get("auth") or entry.get("identitytoken"):
|
|
260
|
+
return True
|
|
261
|
+
if creds_store:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
cred_helpers = config.get("credHelpers", {}) or {}
|
|
265
|
+
if any(key in cred_helpers for key in candidate_keys):
|
|
266
|
+
return True
|
|
240
267
|
|
|
241
268
|
return False
|
|
242
269
|
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/exceptions.py
RENAMED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"""Custom exceptions for ndslive.mapviewer."""
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
_DOCTOR_HINT = (
|
|
5
|
+
"Run 'ndslive-setup doctor' for a diagnosis, or 'ndslive-setup' to "
|
|
6
|
+
"reconfigure."
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
4
10
|
class MapViewerError(Exception):
|
|
5
11
|
"""Base exception for all MapViewer errors."""
|
|
6
12
|
|
|
@@ -16,7 +22,7 @@ class RuntimeNotInstalled(MapViewerError):
|
|
|
16
22
|
"Please install Docker or Podman:\n"
|
|
17
23
|
" Docker: https://docs.docker.com/get-docker/\n"
|
|
18
24
|
" Podman: https://podman.io/getting-started/installation\n\n"
|
|
19
|
-
"
|
|
25
|
+
f"{_DOCTOR_HINT}"
|
|
20
26
|
)
|
|
21
27
|
super().__init__(self.message)
|
|
22
28
|
|
|
@@ -34,8 +40,9 @@ class RuntimeNotRunning(MapViewerError):
|
|
|
34
40
|
"Please start your container runtime:\n"
|
|
35
41
|
" Docker (macOS/Windows): Open Docker Desktop\n"
|
|
36
42
|
" Docker (Linux): sudo systemctl start docker\n"
|
|
43
|
+
" Podman (macOS/Windows): podman machine start\n"
|
|
37
44
|
" Podman (Linux): systemctl --user start podman.socket\n\n"
|
|
38
|
-
"
|
|
45
|
+
f"{_DOCTOR_HINT}"
|
|
39
46
|
)
|
|
40
47
|
super().__init__(self.message)
|
|
41
48
|
|
|
@@ -65,7 +72,7 @@ class RegistryNotLoggedIn(MapViewerError):
|
|
|
65
72
|
" 2. Click your username (top right) -> Edit Profile\n"
|
|
66
73
|
" 3. Under Identity Tokens, click Generate\n"
|
|
67
74
|
f" 4. Use the token as your password for {runtime} login\n\n"
|
|
68
|
-
"
|
|
75
|
+
f"{_DOCTOR_HINT}"
|
|
69
76
|
)
|
|
70
77
|
super().__init__(self.message)
|
|
71
78
|
|
|
@@ -150,6 +157,7 @@ class RegistryQueryFailed(MapViewerError):
|
|
|
150
157
|
"\n\nPossible causes:"
|
|
151
158
|
f"\n - Not logged in: run '{runtime} login {registry}'"
|
|
152
159
|
"\n - Behind a proxy: configure your runtime's proxy settings"
|
|
160
|
+
f"\n\n{_DOCTOR_HINT}"
|
|
153
161
|
)
|
|
154
162
|
self.message = msg
|
|
155
163
|
super().__init__(self.message)
|
|
@@ -152,6 +152,88 @@ class TestIsLoggedIn:
|
|
|
152
152
|
|
|
153
153
|
assert dc.is_logged_in("member") is True
|
|
154
154
|
|
|
155
|
+
def test_creds_store_with_auths_stub_returns_true(self, mocker, temp_dir):
|
|
156
|
+
"""credsStore + an empty auths entry for the registry counts as logged in.
|
|
157
|
+
|
|
158
|
+
Docker CLI writes ``auths[host] = {}`` and stores the real secret in
|
|
159
|
+
the configured credential helper. The helper alone isn't proof (could
|
|
160
|
+
hold creds for any registry), but the combination is.
|
|
161
|
+
"""
|
|
162
|
+
config_path = temp_dir / "config.json"
|
|
163
|
+
config_path.write_text(json.dumps({
|
|
164
|
+
"auths": {"artifactory.nds-association.org": {}},
|
|
165
|
+
"credsStore": "osxkeychain",
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
mock_result = mocker.MagicMock(returncode=1, stdout="")
|
|
169
|
+
mocker.patch.object(rt, "run", return_value=mock_result)
|
|
170
|
+
mocker.patch.object(rt, "get_auth_config_paths", return_value=[config_path])
|
|
171
|
+
|
|
172
|
+
assert dc.is_logged_in("member") is True
|
|
173
|
+
|
|
174
|
+
def test_creds_store_without_registry_auths_returns_false(self, mocker, temp_dir):
|
|
175
|
+
"""credsStore + auths for an unrelated registry does not count."""
|
|
176
|
+
config_path = temp_dir / "config.json"
|
|
177
|
+
config_path.write_text(json.dumps({
|
|
178
|
+
"auths": {"ghcr.io": {}},
|
|
179
|
+
"credsStore": "osxkeychain",
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
mock_result = mocker.MagicMock(returncode=1, stdout="")
|
|
183
|
+
mocker.patch.object(rt, "run", return_value=mock_result)
|
|
184
|
+
mocker.patch.object(rt, "get_auth_config_paths", return_value=[config_path])
|
|
185
|
+
|
|
186
|
+
assert dc.is_logged_in("member") is False
|
|
187
|
+
|
|
188
|
+
def test_namespaced_auth_key_returns_true(self, mocker, temp_dir):
|
|
189
|
+
"""Podman stores auth under the full namespaced key."""
|
|
190
|
+
config_path = temp_dir / "auth.json"
|
|
191
|
+
config_path.write_text(json.dumps({
|
|
192
|
+
"auths": {
|
|
193
|
+
"artifactory.nds-association.org/tooling-dockerreg": {
|
|
194
|
+
"auth": "base64encoded",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}))
|
|
198
|
+
|
|
199
|
+
mock_result = mocker.MagicMock(returncode=1, stdout="")
|
|
200
|
+
mocker.patch.object(rt, "run", return_value=mock_result)
|
|
201
|
+
mocker.patch.object(rt, "get_auth_config_paths", return_value=[config_path])
|
|
202
|
+
|
|
203
|
+
assert dc.is_logged_in("member") is True
|
|
204
|
+
|
|
205
|
+
def test_scheme_prefixed_cred_helper_returns_true(self, mocker, temp_dir):
|
|
206
|
+
"""``https://``-prefixed credHelpers keys are recognized."""
|
|
207
|
+
config_path = temp_dir / "config.json"
|
|
208
|
+
config_path.write_text(json.dumps({
|
|
209
|
+
"credHelpers": {
|
|
210
|
+
"https://artifactory.nds-association.org": "desktop",
|
|
211
|
+
},
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
mock_result = mocker.MagicMock(returncode=1, stdout="")
|
|
215
|
+
mocker.patch.object(rt, "run", return_value=mock_result)
|
|
216
|
+
mocker.patch.object(rt, "get_auth_config_paths", return_value=[config_path])
|
|
217
|
+
|
|
218
|
+
assert dc.is_logged_in("member") is True
|
|
219
|
+
|
|
220
|
+
def test_https_prefixed_auths_key_returns_true(self, mocker, temp_dir):
|
|
221
|
+
"""auths keys stored with an ``https://`` scheme prefix are recognized."""
|
|
222
|
+
config_path = temp_dir / "config.json"
|
|
223
|
+
config_path.write_text(json.dumps({
|
|
224
|
+
"auths": {
|
|
225
|
+
"https://artifactory.nds-association.org/tooling-dockerreg": {
|
|
226
|
+
"auth": "base64encoded",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}))
|
|
230
|
+
|
|
231
|
+
mock_result = mocker.MagicMock(returncode=1, stdout="")
|
|
232
|
+
mocker.patch.object(rt, "run", return_value=mock_result)
|
|
233
|
+
mocker.patch.object(rt, "get_auth_config_paths", return_value=[config_path])
|
|
234
|
+
|
|
235
|
+
assert dc.is_logged_in("member") is True
|
|
236
|
+
|
|
155
237
|
|
|
156
238
|
class TestListContainers:
|
|
157
239
|
"""Tests for list_containers()."""
|
|
@@ -354,9 +436,10 @@ class TestContainerRuntime:
|
|
|
354
436
|
runtime = rt.detect_runtime()
|
|
355
437
|
assert runtime in ("docker", "podman")
|
|
356
438
|
|
|
357
|
-
def test_detect_runtime_prefers_working_runtime(self, mocker):
|
|
439
|
+
def test_detect_runtime_prefers_working_runtime(self, tmp_path, mocker, monkeypatch):
|
|
358
440
|
"""Auto-detection should skip an installed but unhealthy runtime."""
|
|
359
441
|
rt.detect_runtime.cache_clear()
|
|
442
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
360
443
|
mocker.patch.dict(os.environ, {"CONTAINER_RUNTIME": ""}, clear=False)
|
|
361
444
|
mocker.patch.object(rt.shutil, "which", side_effect=lambda name: f"/usr/bin/{name}")
|
|
362
445
|
|
|
@@ -375,9 +458,10 @@ class TestContainerRuntime:
|
|
|
375
458
|
finally:
|
|
376
459
|
rt.detect_runtime.cache_clear()
|
|
377
460
|
|
|
378
|
-
def test_detect_runtime_explicit_env_forces_choice(self, mocker):
|
|
461
|
+
def test_detect_runtime_explicit_env_forces_choice(self, tmp_path, mocker, monkeypatch):
|
|
379
462
|
"""CONTAINER_RUNTIME should force the selected runtime."""
|
|
380
463
|
rt.detect_runtime.cache_clear()
|
|
464
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
381
465
|
mocker.patch.dict(os.environ, {"CONTAINER_RUNTIME": "docker"}, clear=False)
|
|
382
466
|
mocker.patch.object(rt.shutil, "which", side_effect=lambda name: f"/usr/bin/{name}")
|
|
383
467
|
mocker.patch.object(rt.subprocess, "run", side_effect=AssertionError("should not probe"))
|
|
@@ -386,9 +470,12 @@ class TestContainerRuntime:
|
|
|
386
470
|
finally:
|
|
387
471
|
rt.detect_runtime.cache_clear()
|
|
388
472
|
|
|
389
|
-
def test_detect_runtime_returns_first_installed_if_none_respond(
|
|
473
|
+
def test_detect_runtime_returns_first_installed_if_none_respond(
|
|
474
|
+
self, tmp_path, mocker, monkeypatch,
|
|
475
|
+
):
|
|
390
476
|
"""When no runtime is responsive, keep the first installed for later errors."""
|
|
391
477
|
rt.detect_runtime.cache_clear()
|
|
478
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
392
479
|
mocker.patch.dict(os.environ, {"CONTAINER_RUNTIME": ""}, clear=False)
|
|
393
480
|
mocker.patch.object(rt.shutil, "which", side_effect=lambda name: f"/usr/bin/{name}")
|
|
394
481
|
mocker.patch.object(
|
|
@@ -416,6 +503,144 @@ class TestContainerRuntime:
|
|
|
416
503
|
assert isinstance(paths, list)
|
|
417
504
|
assert all(isinstance(p, type(paths[0])) for p in paths)
|
|
418
505
|
|
|
506
|
+
def test_detect_runtime_honours_setup_json(self, tmp_path, mocker, monkeypatch):
|
|
507
|
+
"""A runtime_name in ~/.nds/setup.json wins over env/autodetect."""
|
|
508
|
+
rt.detect_runtime.cache_clear()
|
|
509
|
+
setup_dir = tmp_path / ".nds"
|
|
510
|
+
setup_dir.mkdir()
|
|
511
|
+
(setup_dir / "setup.json").write_text(json.dumps({"runtime_name": "podman"}))
|
|
512
|
+
|
|
513
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
514
|
+
monkeypatch.setenv("CONTAINER_RUNTIME", "docker")
|
|
515
|
+
mocker.patch.object(rt.shutil, "which", side_effect=lambda name: f"/usr/bin/{name}")
|
|
516
|
+
mocker.patch.object(
|
|
517
|
+
rt.subprocess,
|
|
518
|
+
"run",
|
|
519
|
+
side_effect=AssertionError("should not probe when saved pref wins"),
|
|
520
|
+
)
|
|
521
|
+
try:
|
|
522
|
+
assert rt.detect_runtime() == "podman"
|
|
523
|
+
finally:
|
|
524
|
+
rt.detect_runtime.cache_clear()
|
|
525
|
+
|
|
526
|
+
def test_detect_runtime_falls_back_when_saved_missing(
|
|
527
|
+
self, tmp_path, mocker, monkeypatch,
|
|
528
|
+
):
|
|
529
|
+
"""If the saved runtime isn't on PATH, fall back to env/autodetect."""
|
|
530
|
+
rt.detect_runtime.cache_clear()
|
|
531
|
+
setup_dir = tmp_path / ".nds"
|
|
532
|
+
setup_dir.mkdir()
|
|
533
|
+
(setup_dir / "setup.json").write_text(json.dumps({"runtime_name": "podman"}))
|
|
534
|
+
|
|
535
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
536
|
+
monkeypatch.delenv("CONTAINER_RUNTIME", raising=False)
|
|
537
|
+
mocker.patch.object(
|
|
538
|
+
rt.shutil,
|
|
539
|
+
"which",
|
|
540
|
+
side_effect=lambda name: "/usr/bin/docker" if name == "docker" else None,
|
|
541
|
+
)
|
|
542
|
+
mocker.patch.object(
|
|
543
|
+
rt.subprocess,
|
|
544
|
+
"run",
|
|
545
|
+
return_value=subprocess.CompletedProcess(["docker", "info"], 0, "", ""),
|
|
546
|
+
)
|
|
547
|
+
try:
|
|
548
|
+
assert rt.detect_runtime() == "docker"
|
|
549
|
+
finally:
|
|
550
|
+
rt.detect_runtime.cache_clear()
|
|
551
|
+
|
|
552
|
+
def test_detect_runtime_ignores_malformed_setup_json(
|
|
553
|
+
self, tmp_path, mocker, monkeypatch,
|
|
554
|
+
):
|
|
555
|
+
"""Garbled ~/.nds/setup.json must not break detection."""
|
|
556
|
+
rt.detect_runtime.cache_clear()
|
|
557
|
+
setup_dir = tmp_path / ".nds"
|
|
558
|
+
setup_dir.mkdir()
|
|
559
|
+
(setup_dir / "setup.json").write_text("{ not valid json")
|
|
560
|
+
|
|
561
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
562
|
+
monkeypatch.delenv("CONTAINER_RUNTIME", raising=False)
|
|
563
|
+
mocker.patch.object(
|
|
564
|
+
rt.shutil,
|
|
565
|
+
"which",
|
|
566
|
+
side_effect=lambda name: "/usr/bin/docker" if name == "docker" else None,
|
|
567
|
+
)
|
|
568
|
+
mocker.patch.object(
|
|
569
|
+
rt.subprocess,
|
|
570
|
+
"run",
|
|
571
|
+
return_value=subprocess.CompletedProcess(["docker", "info"], 0, "", ""),
|
|
572
|
+
)
|
|
573
|
+
try:
|
|
574
|
+
assert rt.detect_runtime() == "docker"
|
|
575
|
+
finally:
|
|
576
|
+
rt.detect_runtime.cache_clear()
|
|
577
|
+
|
|
578
|
+
def test_detect_runtime_ignores_unknown_runtime_name(
|
|
579
|
+
self, tmp_path, mocker, monkeypatch,
|
|
580
|
+
):
|
|
581
|
+
"""An unrecognised runtime_name in setup.json is ignored, not rejected."""
|
|
582
|
+
rt.detect_runtime.cache_clear()
|
|
583
|
+
setup_dir = tmp_path / ".nds"
|
|
584
|
+
setup_dir.mkdir()
|
|
585
|
+
(setup_dir / "setup.json").write_text(json.dumps({"runtime_name": "rkt"}))
|
|
586
|
+
|
|
587
|
+
monkeypatch.setattr(rt.Path, "home", lambda: tmp_path)
|
|
588
|
+
monkeypatch.delenv("CONTAINER_RUNTIME", raising=False)
|
|
589
|
+
mocker.patch.object(
|
|
590
|
+
rt.shutil,
|
|
591
|
+
"which",
|
|
592
|
+
side_effect=lambda name: "/usr/bin/docker" if name == "docker" else None,
|
|
593
|
+
)
|
|
594
|
+
mocker.patch.object(
|
|
595
|
+
rt.subprocess,
|
|
596
|
+
"run",
|
|
597
|
+
return_value=subprocess.CompletedProcess(["docker", "info"], 0, "", ""),
|
|
598
|
+
)
|
|
599
|
+
try:
|
|
600
|
+
assert rt.detect_runtime() == "docker"
|
|
601
|
+
finally:
|
|
602
|
+
rt.detect_runtime.cache_clear()
|
|
603
|
+
|
|
604
|
+
def test_podman_auth_paths_exclude_docker_config(self, monkeypatch):
|
|
605
|
+
"""Podman should not consider ~/.docker/config.json as a login source.
|
|
606
|
+
|
|
607
|
+
On macOS, Docker Desktop's credsStore runs on the host and podman
|
|
608
|
+
cannot invoke it — so a docker login must not imply a podman login.
|
|
609
|
+
"""
|
|
610
|
+
rt.detect_runtime.cache_clear()
|
|
611
|
+
monkeypatch.setenv("CONTAINER_RUNTIME", "podman")
|
|
612
|
+
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
|
|
613
|
+
monkeypatch.delenv("APPDATA", raising=False)
|
|
614
|
+
# shutil.which needs to find podman for detect_runtime to accept env.
|
|
615
|
+
import shutil as _sh
|
|
616
|
+
monkeypatch.setattr(
|
|
617
|
+
rt.shutil, "which",
|
|
618
|
+
lambda name: "/usr/bin/podman" if name == "podman" else None,
|
|
619
|
+
)
|
|
620
|
+
try:
|
|
621
|
+
paths = rt.get_auth_config_paths()
|
|
622
|
+
docker_config = rt.Path.home() / ".docker" / "config.json"
|
|
623
|
+
assert docker_config not in paths
|
|
624
|
+
finally:
|
|
625
|
+
rt.detect_runtime.cache_clear()
|
|
626
|
+
|
|
627
|
+
def test_podman_auth_paths_include_appdata_on_windows(self, tmp_path, monkeypatch):
|
|
628
|
+
"""When APPDATA is set (Windows), podman auth.json under it is probed."""
|
|
629
|
+
rt.detect_runtime.cache_clear()
|
|
630
|
+
monkeypatch.setenv("CONTAINER_RUNTIME", "podman")
|
|
631
|
+
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
|
632
|
+
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
|
|
633
|
+
monkeypatch.setattr(
|
|
634
|
+
rt.shutil, "which",
|
|
635
|
+
lambda name: "/usr/bin/podman" if name == "podman" else None,
|
|
636
|
+
)
|
|
637
|
+
try:
|
|
638
|
+
paths = rt.get_auth_config_paths()
|
|
639
|
+
expected = rt.Path(str(tmp_path / "AppData" / "Roaming")) / "containers" / "auth.json"
|
|
640
|
+
assert expected in paths
|
|
641
|
+
finally:
|
|
642
|
+
rt.detect_runtime.cache_clear()
|
|
643
|
+
|
|
419
644
|
def test_parse_size_string(self):
|
|
420
645
|
"""Parse common size format strings."""
|
|
421
646
|
assert rt.parse_size_string("256MiB") == 256 * 1024 * 1024
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/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.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/__init__.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/browser.py
RENAMED
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev45 → nds_mapviewer-2026.1.3.dev47}/src/ndslive/mapviewer/viewer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|