nds-mapviewer 2026.1.3.dev43__tar.gz → 2026.1.3.dev45__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.dev43/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.3.dev45}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45/src/nds_mapviewer.egg-info}/PKG-INFO +1 -1
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/docker_client.py +34 -10
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/viewer.py +21 -3
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/tests/test_docker_client.py +52 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/.gitignore +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/CHANGELOG.md +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/README.md +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/pyproject.toml +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/setup.cfg +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/setup.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/browser.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/cli.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/config.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/container_runtime.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/exceptions.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/tui.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/tests/conftest.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/tests/test_cli.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/tests/test_config.py +0 -0
- {nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/tests/test_viewer.py +0 -0
{nds_mapviewer-2026.1.3.dev43/src/nds_mapviewer.egg-info → nds_mapviewer-2026.1.3.dev45}/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.dev45
|
|
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.dev43 → nds_mapviewer-2026.1.3.dev45/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.dev45
|
|
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.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/docker_client.py
RENAMED
|
@@ -695,7 +695,7 @@ def get_container_logs(
|
|
|
695
695
|
raise ContainerNotFound(name)
|
|
696
696
|
|
|
697
697
|
if follow:
|
|
698
|
-
return
|
|
698
|
+
return ContainerLogStream(rt.stream("logs", "--follow", name))
|
|
699
699
|
else:
|
|
700
700
|
cmd = ["logs"]
|
|
701
701
|
if tail != "all":
|
|
@@ -712,15 +712,39 @@ def get_container_logs(
|
|
|
712
712
|
return logs
|
|
713
713
|
|
|
714
714
|
|
|
715
|
-
|
|
716
|
-
"""
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
715
|
+
class ContainerLogStream:
|
|
716
|
+
"""Iterable log stream that can be closed from outside the iterator.
|
|
717
|
+
|
|
718
|
+
The underlying ``<runtime> logs --follow`` subprocess blocks in
|
|
719
|
+
``proc.stdout`` until the container exits. Docker closes the pipe
|
|
720
|
+
promptly on ``stop``; Podman can keep the follower alive longer, so we
|
|
721
|
+
expose :meth:`close` to terminate it deterministically on shutdown.
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
def __init__(self, proc: subprocess.Popen[str]) -> None:
|
|
725
|
+
self._proc = proc
|
|
726
|
+
|
|
727
|
+
def __iter__(self) -> Iterator[str]:
|
|
728
|
+
try:
|
|
729
|
+
for line in self._proc.stdout:
|
|
730
|
+
yield line
|
|
731
|
+
finally:
|
|
732
|
+
self.close()
|
|
733
|
+
|
|
734
|
+
def close(self, timeout: float = 3.0) -> None:
|
|
735
|
+
if self._proc.poll() is not None:
|
|
736
|
+
return
|
|
737
|
+
self._proc.terminate()
|
|
738
|
+
try:
|
|
739
|
+
self._proc.wait(timeout=timeout)
|
|
740
|
+
return
|
|
741
|
+
except subprocess.TimeoutExpired:
|
|
742
|
+
pass
|
|
743
|
+
self._proc.kill()
|
|
744
|
+
try:
|
|
745
|
+
self._proc.wait(timeout=timeout)
|
|
746
|
+
except subprocess.TimeoutExpired:
|
|
747
|
+
pass
|
|
724
748
|
|
|
725
749
|
|
|
726
750
|
_READY_SENTINEL = "NDS MapViewer is ready!"
|
{nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/viewer.py
RENAMED
|
@@ -16,7 +16,7 @@ from .config import (
|
|
|
16
16
|
validate_paths,
|
|
17
17
|
write_config_to_temp,
|
|
18
18
|
)
|
|
19
|
-
from .docker_client import ContainerInfo
|
|
19
|
+
from .docker_client import ContainerInfo, ContainerLogStream
|
|
20
20
|
from .exceptions import ConfigError, ContainerNotFound
|
|
21
21
|
|
|
22
22
|
|
|
@@ -84,6 +84,7 @@ class MapViewer:
|
|
|
84
84
|
instance._bind_address = "127.0.0.1"
|
|
85
85
|
instance._original_config = None
|
|
86
86
|
instance._image_tag = info.image or None
|
|
87
|
+
instance._log_stream = None
|
|
87
88
|
return instance
|
|
88
89
|
|
|
89
90
|
def __init__(
|
|
@@ -155,6 +156,7 @@ class MapViewer:
|
|
|
155
156
|
self._container: ContainerInfo | None = None
|
|
156
157
|
self._original_config: dict | None = None
|
|
157
158
|
self._image_tag: str | None = None
|
|
159
|
+
self._log_stream: ContainerLogStream | None = None
|
|
158
160
|
|
|
159
161
|
@property
|
|
160
162
|
def url(self) -> str:
|
|
@@ -322,11 +324,22 @@ class MapViewer:
|
|
|
322
324
|
Args:
|
|
323
325
|
remove: Also remove the container after stopping
|
|
324
326
|
"""
|
|
327
|
+
self._close_log_stream()
|
|
325
328
|
try:
|
|
326
329
|
docker_client.stop_container(self._name, remove=remove)
|
|
327
330
|
finally:
|
|
328
331
|
self._cleanup()
|
|
329
332
|
|
|
333
|
+
def _close_log_stream(self) -> None:
|
|
334
|
+
stream = getattr(self, "_log_stream", None)
|
|
335
|
+
if stream is None:
|
|
336
|
+
return
|
|
337
|
+
try:
|
|
338
|
+
stream.close()
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
self._log_stream = None
|
|
342
|
+
|
|
330
343
|
def restart(self, pull: bool = False, wait: bool = True, timeout: int = 30) -> None:
|
|
331
344
|
"""
|
|
332
345
|
Restart the MapViewer container.
|
|
@@ -361,10 +374,15 @@ class MapViewer:
|
|
|
361
374
|
tail: Number of lines from end, or "all"
|
|
362
375
|
|
|
363
376
|
Returns:
|
|
364
|
-
If follow=True, returns
|
|
377
|
+
If follow=True, returns a :class:`ContainerLogStream` (iterable,
|
|
378
|
+
closed automatically by :meth:`stop`).
|
|
365
379
|
If follow=False, returns the full log as a string.
|
|
366
380
|
"""
|
|
367
|
-
|
|
381
|
+
result = docker_client.get_container_logs(self._name, follow=follow, tail=tail)
|
|
382
|
+
if follow and isinstance(result, ContainerLogStream):
|
|
383
|
+
self._close_log_stream()
|
|
384
|
+
self._log_stream = result
|
|
385
|
+
return result
|
|
368
386
|
|
|
369
387
|
def get_datasources(self) -> list[str]:
|
|
370
388
|
"""
|
|
@@ -198,6 +198,58 @@ class TestStopContainer:
|
|
|
198
198
|
dc.stop_container("nonexistent-container-xyz123")
|
|
199
199
|
|
|
200
200
|
|
|
201
|
+
class TestContainerLogStream:
|
|
202
|
+
"""Tests for ContainerLogStream.close() teardown behaviour."""
|
|
203
|
+
|
|
204
|
+
def test_close_skips_when_already_exited(self, mocker):
|
|
205
|
+
proc = mocker.MagicMock()
|
|
206
|
+
proc.poll.return_value = 0
|
|
207
|
+
stream = dc.ContainerLogStream(proc)
|
|
208
|
+
|
|
209
|
+
stream.close()
|
|
210
|
+
|
|
211
|
+
proc.terminate.assert_not_called()
|
|
212
|
+
proc.kill.assert_not_called()
|
|
213
|
+
|
|
214
|
+
def test_close_terminates_and_waits(self, mocker):
|
|
215
|
+
proc = mocker.MagicMock()
|
|
216
|
+
proc.poll.return_value = None
|
|
217
|
+
proc.wait.return_value = 0
|
|
218
|
+
stream = dc.ContainerLogStream(proc)
|
|
219
|
+
|
|
220
|
+
stream.close(timeout=1.5)
|
|
221
|
+
|
|
222
|
+
proc.terminate.assert_called_once()
|
|
223
|
+
proc.wait.assert_called_once_with(timeout=1.5)
|
|
224
|
+
proc.kill.assert_not_called()
|
|
225
|
+
|
|
226
|
+
def test_close_kills_when_terminate_times_out(self, mocker):
|
|
227
|
+
proc = mocker.MagicMock()
|
|
228
|
+
proc.poll.return_value = None
|
|
229
|
+
proc.wait.side_effect = [
|
|
230
|
+
subprocess.TimeoutExpired(cmd="logs", timeout=1.0),
|
|
231
|
+
0,
|
|
232
|
+
]
|
|
233
|
+
stream = dc.ContainerLogStream(proc)
|
|
234
|
+
|
|
235
|
+
stream.close(timeout=1.0)
|
|
236
|
+
|
|
237
|
+
proc.terminate.assert_called_once()
|
|
238
|
+
proc.kill.assert_called_once()
|
|
239
|
+
assert proc.wait.call_count == 2
|
|
240
|
+
|
|
241
|
+
def test_close_returns_even_if_kill_wait_times_out(self, mocker):
|
|
242
|
+
proc = mocker.MagicMock()
|
|
243
|
+
proc.poll.return_value = None
|
|
244
|
+
proc.wait.side_effect = subprocess.TimeoutExpired(cmd="logs", timeout=1.0)
|
|
245
|
+
stream = dc.ContainerLogStream(proc)
|
|
246
|
+
|
|
247
|
+
stream.close(timeout=1.0)
|
|
248
|
+
|
|
249
|
+
proc.terminate.assert_called_once()
|
|
250
|
+
proc.kill.assert_called_once()
|
|
251
|
+
|
|
252
|
+
|
|
201
253
|
class TestPullImageErrorHandling:
|
|
202
254
|
"""Tests for pull_image() error handling."""
|
|
203
255
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/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.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/__init__.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/browser.py
RENAMED
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/src/ndslive/mapviewer/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.1.3.dev43 → nds_mapviewer-2026.1.3.dev45}/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
|