nds-mapviewer 2026.2.1.dev93__tar.gz → 2026.2.1.dev94__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 (31) hide show
  1. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/CHANGELOG.md +8 -0
  2. {nds_mapviewer-2026.2.1.dev93/src/nds_mapviewer.egg-info → nds_mapviewer-2026.2.1.dev94}/PKG-INFO +15 -1
  3. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/README.md +14 -0
  4. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94/src/nds_mapviewer.egg-info}/PKG-INFO +15 -1
  5. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/cli.py +17 -1
  6. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/config.py +31 -0
  7. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_cli.py +50 -0
  8. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_config.py +71 -0
  9. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/.gitignore +0 -0
  10. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/pyproject.toml +0 -0
  11. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/setup.cfg +0 -0
  12. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/setup.py +0 -0
  13. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
  14. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
  15. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
  16. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/requires.txt +0 -0
  17. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
  18. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/__init__.py +0 -0
  19. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/browser.py +0 -0
  20. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/clipboard.py +0 -0
  21. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/container_runtime.py +0 -0
  22. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/docker_client.py +0 -0
  23. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/exceptions.py +0 -0
  24. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/tui.py +0 -0
  25. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/ui.py +0 -0
  26. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/viewer.py +0 -0
  27. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/conftest.py +0 -0
  28. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_clipboard.py +0 -0
  29. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_docker_client.py +0 -0
  30. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_tui.py +0 -0
  31. {nds_mapviewer-2026.2.1.dev93 → nds_mapviewer-2026.2.1.dev94}/tests/test_viewer.py +0 -0
@@ -18,6 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
18
  - Run screen now shows a config line below the info banner with the source
19
19
  (config file or directly-passed data-source path, e.g. a `*.ndslive`
20
20
  filestore) and the mapped config mounted into the container
21
+ - Accept an http(s) service URL directly as a source (e.g.
22
+ `mapviewer https://my-host/openapi.json`), turned into a
23
+ `SmartLayerTileService`; the bare-URL form configures no auth — use
24
+ ndslive-server as a proxy or a config.yaml with `http-settings` for
25
+ authenticated services
21
26
  - Default Docker image version pinned to package (`--image-version` defaults to
22
27
  matching calver instead of `"latest"`)
23
28
  - SCM-based versioning — package version derived from MapViewer git tags
@@ -31,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
31
36
  - Quitting the TUI no longer hangs — the log-follow stream is torn down by
32
37
  terminating the process before closing pipes, and container shutdown is
33
38
  bounded so the terminal is always returned to the user
39
+ - `localhost`/`127.0.0.1` in service `uri` values and `http-settings` `scope`
40
+ values are now translated to `host.docker.internal`, so a service running on
41
+ the host machine is reachable from the container without `--network host`
34
42
  - Container readiness check uses log sentinel before HTTP polling to avoid
35
43
  startup crash (mapget#151)
36
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.2.1.dev93
3
+ Version: 2026.2.1.dev94
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
@@ -74,6 +74,7 @@ mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml
74
74
  mapviewer /path/to/data.ndslive # View a local FileStore
75
75
  mapviewer /path/to/folder # View a local SmartLayer folder
76
76
  mapviewer /path/to/geojson/ # View a GeoJSON folder
77
+ mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
77
78
  mapviewer --demo # Try it with NDS Islands sample data
78
79
 
79
80
  mapviewer mywork # Load a saved config by name
@@ -86,6 +87,19 @@ mapviewer --demo --network host # Run the container on the host network
86
87
 
87
88
  All commands accept `-e community|member` and `-i VERSION`.
88
89
 
90
+ ### Remote service URLs
91
+
92
+ A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
93
+ which is turned into a `SmartLayerTileService`. The bare-URL form configures **no
94
+ authentication** — for protected services, proxy them through `ndslive-server` or
95
+ declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
96
+ bearer, OAuth2, proxy).
97
+
98
+ For a service running on your own machine, use `localhost`/`127.0.0.1`
99
+ (e.g. `mapviewer http://127.0.0.1:8080/openapi.json`) — it is automatically
100
+ rewritten to `host.docker.internal` so the container can reach it. You do **not**
101
+ need `--network host` for this.
102
+
89
103
  ### Container environment variables
90
104
 
91
105
  `--debug` turns on verbose logging in the MapViewer container (shorthand for
@@ -41,6 +41,7 @@ mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml
41
41
  mapviewer /path/to/data.ndslive # View a local FileStore
42
42
  mapviewer /path/to/folder # View a local SmartLayer folder
43
43
  mapviewer /path/to/geojson/ # View a GeoJSON folder
44
+ mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
44
45
  mapviewer --demo # Try it with NDS Islands sample data
45
46
 
46
47
  mapviewer mywork # Load a saved config by name
@@ -53,6 +54,19 @@ mapviewer --demo --network host # Run the container on the host network
53
54
 
54
55
  All commands accept `-e community|member` and `-i VERSION`.
55
56
 
57
+ ### Remote service URLs
58
+
59
+ A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
60
+ which is turned into a `SmartLayerTileService`. The bare-URL form configures **no
61
+ authentication** — for protected services, proxy them through `ndslive-server` or
62
+ declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
63
+ bearer, OAuth2, proxy).
64
+
65
+ For a service running on your own machine, use `localhost`/`127.0.0.1`
66
+ (e.g. `mapviewer http://127.0.0.1:8080/openapi.json`) — it is automatically
67
+ rewritten to `host.docker.internal` so the container can reach it. You do **not**
68
+ need `--network host` for this.
69
+
56
70
  ### Container environment variables
57
71
 
58
72
  `--debug` turns on verbose logging in the MapViewer container (shorthand for
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.2.1.dev93
3
+ Version: 2026.2.1.dev94
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
@@ -74,6 +74,7 @@ mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml
74
74
  mapviewer /path/to/data.ndslive # View a local FileStore
75
75
  mapviewer /path/to/folder # View a local SmartLayer folder
76
76
  mapviewer /path/to/geojson/ # View a GeoJSON folder
77
+ mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
77
78
  mapviewer --demo # Try it with NDS Islands sample data
78
79
 
79
80
  mapviewer mywork # Load a saved config by name
@@ -86,6 +87,19 @@ mapviewer --demo --network host # Run the container on the host network
86
87
 
87
88
  All commands accept `-e community|member` and `-i VERSION`.
88
89
 
90
+ ### Remote service URLs
91
+
92
+ A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
93
+ which is turned into a `SmartLayerTileService`. The bare-URL form configures **no
94
+ authentication** — for protected services, proxy them through `ndslive-server` or
95
+ declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
96
+ bearer, OAuth2, proxy).
97
+
98
+ For a service running on your own machine, use `localhost`/`127.0.0.1`
99
+ (e.g. `mapviewer http://127.0.0.1:8080/openapi.json`) — it is automatically
100
+ rewritten to `host.docker.internal` so the container can reach it. You do **not**
101
+ need `--network host` for this.
102
+
89
103
  ### Container environment variables
90
104
 
91
105
  `--debug` turns on verbose logging in the MapViewer container (shorthand for
@@ -117,6 +117,7 @@ def cmd_main(args: argparse.Namespace) -> int:
117
117
  create_folder_config,
118
118
  create_geojson_config,
119
119
  create_classic_config,
120
+ create_service_config,
120
121
  get_demo_config,
121
122
  )
122
123
  from .tui import MapViewerApp
@@ -219,6 +220,18 @@ def cmd_main(args: argparse.Namespace) -> int:
219
220
  source_config_path: Path | None = None
220
221
 
221
222
  for source_arg in source_args:
223
+ # Remote service: an http(s) URL is turned into a SmartLayerTileService
224
+ # source directly. Checked before the path:mapId split below because
225
+ # URLs contain colons. No auth is configured for the bare-URL form —
226
+ # use ndslive-server as a proxy or a config.yaml for authenticated
227
+ # services.
228
+ if source_arg.startswith(("http://", "https://")):
229
+ config = create_service_config(source_arg)
230
+ descriptions.append(f"service: {source_arg}")
231
+ all_sources.extend(config.get("sources", []))
232
+ all_http_settings.extend(config.get("http-settings", []))
233
+ continue
234
+
222
235
  # Parse optional path:mapId syntax
223
236
  custom_map_id = None
224
237
  if ":" in source_arg:
@@ -313,6 +326,9 @@ Examples:
313
326
  mapviewer /path/to/geojson/folder
314
327
  mapviewer --demo
315
328
 
329
+ # Connect to a remote service (unauthenticated)
330
+ mapviewer https://my-host/openapi.json
331
+
316
332
  # Multiple sources at once
317
333
  mapviewer /path/a.ndslive /path/to/geojson/
318
334
 
@@ -335,7 +351,7 @@ Examples:
335
351
  parser.add_argument(
336
352
  "sources",
337
353
  nargs="*",
338
- help="One or more data source paths or saved config names",
354
+ help="One or more data source paths, http(s) service URLs, or saved config names",
339
355
  )
340
356
  parser.add_argument(
341
357
  "--demo",
@@ -138,6 +138,22 @@ def _transform_localhost(host: str) -> str:
138
138
  return host
139
139
 
140
140
 
141
+ def _transform_localhost_url(url: str) -> str:
142
+ """Rewrite a localhost host in an http(s) URL to host.docker.internal.
143
+
144
+ Used for source ``uri`` values and ``http-settings`` ``scope`` values so a
145
+ service running on the host machine (e.g. ``http://127.0.0.1:8080/...``) is
146
+ reachable from inside the container. Non-http(s) values and non-localhost
147
+ hosts are returned unchanged.
148
+ """
149
+ parsed = urlparse(url)
150
+ if parsed.scheme not in ("http", "https"):
151
+ return url
152
+ if not _is_localhost(parsed.netloc):
153
+ return url
154
+ return parsed._replace(netloc=_transform_localhost(parsed.netloc)).geturl()
155
+
156
+
141
157
  def _is_external_or_bundle_url(value: str) -> bool:
142
158
  """Return True for URLs that should not be rewritten as local files."""
143
159
  return value.startswith(("http://", "https://", "bundle"))
@@ -313,6 +329,12 @@ def transform_config(
313
329
  if local_path:
314
330
  local_paths.append(local_path)
315
331
  path_locations.append(("sources", idx, "uri", uri))
332
+ else:
333
+ # Remote service on the host: localhost/127.0.0.1 must become
334
+ # host.docker.internal to be reachable from inside the container.
335
+ new_uri = _transform_localhost_url(uri)
336
+ if new_uri != uri:
337
+ transformed["sources"][idx]["uri"] = new_uri
316
338
 
317
339
  # Check folder field (ClassicFolder, GeoJsonFolder)
318
340
  folder = source.get("folder", "")
@@ -327,6 +349,15 @@ def transform_config(
327
349
  if host and _is_localhost(host):
328
350
  transformed["sources"][idx]["host"] = _transform_localhost(host)
329
351
 
352
+ # Translate localhost in http-settings scopes so auth settings keep
353
+ # matching the rewritten source URIs.
354
+ for setting in transformed.get("http-settings", []) or []:
355
+ scope = setting.get("scope", "")
356
+ if scope:
357
+ new_scope = _transform_localhost_url(scope)
358
+ if new_scope != scope:
359
+ setting["scope"] = new_scope
360
+
330
361
  # Find optimal mount points
331
362
  path_to_mount_parent = _find_common_parent(local_paths)
332
363
 
@@ -128,6 +128,56 @@ class TestCliSourceValidation:
128
128
  assert "not found" in output.lower() or "error" in output.lower()
129
129
 
130
130
 
131
+ class TestCliUrlSource:
132
+ """Tests for passing an http(s) service URL as a source."""
133
+
134
+ def _run_cmd_main(self, monkeypatch, sources):
135
+ from ndslive.mapviewer.cli import _build_parser, cmd_main
136
+
137
+ captured = {}
138
+
139
+ def fake_run(self):
140
+ captured["config"] = self._run_config
141
+ captured["source_paths"] = self._run_source_paths
142
+ captured["label"] = self._run_label
143
+
144
+ monkeypatch.setattr(
145
+ "ndslive.mapviewer.tui.MapViewerApp.run", fake_run,
146
+ )
147
+ args = _build_parser().parse_args([*sources, "--no-pull"])
148
+ rc = cmd_main(args)
149
+ return rc, captured
150
+
151
+ def test_help_mentions_url(self):
152
+ result = run_cli("--help")
153
+ assert "URL" in result.stdout or "openapi.json" in result.stdout
154
+
155
+ def test_url_becomes_smartlayer_source(self, monkeypatch):
156
+ rc, cap = self._run_cmd_main(
157
+ monkeypatch, ["https://my-host/openapi.json"],
158
+ )
159
+ assert rc == 0
160
+ sources = cap["config"]["sources"]
161
+ assert len(sources) == 1
162
+ assert sources[0]["type"] == "SmartLayerTileService"
163
+ assert sources[0]["uri"] == "https://my-host/openapi.json"
164
+ # bare URL → no auth configured
165
+ assert "http-settings" not in cap["config"]
166
+
167
+ def test_url_shown_as_source_path(self, monkeypatch):
168
+ _, cap = self._run_cmd_main(
169
+ monkeypatch, ["https://my-host/openapi.json"],
170
+ )
171
+ assert cap["source_paths"] == ["https://my-host/openapi.json"]
172
+ assert "service:" in cap["label"]
173
+
174
+ def test_http_and_https_both_recognized(self, monkeypatch):
175
+ for url in ("http://h/openapi.json", "https://h/openapi.json"):
176
+ rc, cap = self._run_cmd_main(monkeypatch, [url])
177
+ assert rc == 0
178
+ assert cap["config"]["sources"][0]["uri"] == url
179
+
180
+
131
181
  class TestCliLegacyMigration:
132
182
  """Tests for legacy mapviewer.yaml migration hint."""
133
183
 
@@ -10,6 +10,7 @@ from ndslive.mapviewer.config import (
10
10
  _extract_local_path,
11
11
  _is_localhost,
12
12
  _transform_localhost,
13
+ _transform_localhost_url,
13
14
  create_classic_config,
14
15
  create_filestore_config,
15
16
  create_folder_config,
@@ -166,6 +167,76 @@ class TestTransformLocalhost:
166
167
  assert _transform_localhost("api.nds.live") == "api.nds.live"
167
168
 
168
169
 
170
+ class TestTransformLocalhostUrl:
171
+ """Tests for _transform_localhost_url()."""
172
+
173
+ def test_127_0_0_1_with_port(self):
174
+ assert _transform_localhost_url("http://127.0.0.1:8080/openapi.json") == \
175
+ "http://host.docker.internal:8080/openapi.json"
176
+
177
+ def test_localhost_https(self):
178
+ assert _transform_localhost_url("https://localhost:443/s") == \
179
+ "https://host.docker.internal:443/s"
180
+
181
+ def test_scope_wildcard(self):
182
+ assert _transform_localhost_url("http://localhost:8080/*") == \
183
+ "http://host.docker.internal:8080/*"
184
+
185
+ def test_remote_host_untouched(self):
186
+ url = "https://api.nds.live/island6/openapi.json"
187
+ assert _transform_localhost_url(url) == url
188
+
189
+ def test_non_http_scheme_untouched(self):
190
+ assert _transform_localhost_url("filestore:/data/x.ndslive") == \
191
+ "filestore:/data/x.ndslive"
192
+
193
+
194
+ class TestTransformConfigLocalhostUri:
195
+ """Localhost in source uri / http-settings scope -> host.docker.internal."""
196
+
197
+ def test_uri_localhost_rewritten(self):
198
+ config = {
199
+ "sources": [{
200
+ "type": "SmartLayerTileService",
201
+ "uri": "http://127.0.0.1:8080/openapi.json",
202
+ "mapId": "M",
203
+ }]
204
+ }
205
+ result = transform_config(config)
206
+ assert result.config["sources"][0]["uri"] == \
207
+ "http://host.docker.internal:8080/openapi.json"
208
+ assert result.volumes == []
209
+
210
+ def test_scope_follows_uri(self):
211
+ config = {
212
+ "sources": [{
213
+ "type": "SmartLayerTileService",
214
+ "uri": "http://localhost:8080/openapi.json",
215
+ "mapId": "M",
216
+ }],
217
+ "http-settings": [{"scope": "http://localhost:8080/*", "api-key": "K"}],
218
+ }
219
+ result = transform_config(config)
220
+ assert result.config["sources"][0]["uri"] == \
221
+ "http://host.docker.internal:8080/openapi.json"
222
+ assert result.config["http-settings"][0]["scope"] == \
223
+ "http://host.docker.internal:8080/*"
224
+ # auth value preserved
225
+ assert result.config["http-settings"][0]["api-key"] == "K"
226
+
227
+ def test_remote_uri_untouched(self):
228
+ config = {
229
+ "sources": [{
230
+ "type": "SmartLayerTileService",
231
+ "uri": "https://api.nds.live/island6/openapi.json",
232
+ "mapId": "M",
233
+ }]
234
+ }
235
+ result = transform_config(config)
236
+ assert result.config["sources"][0]["uri"] == \
237
+ "https://api.nds.live/island6/openapi.json"
238
+
239
+
169
240
  class TestTransformConfig:
170
241
  """Tests for transform_config()."""
171
242