nds-mapviewer 2026.4.3.dev251__tar.gz → 2026.4.3.dev253__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.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/CHANGELOG.md +5 -0
- {nds_mapviewer-2026.4.3.dev251/src/nds_mapviewer.egg-info → nds_mapviewer-2026.4.3.dev253}/PKG-INFO +13 -4
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/README.md +12 -3
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253/src/nds_mapviewer.egg-info}/PKG-INFO +13 -4
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/cli.py +16 -6
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/config.py +70 -5
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/tui.py +3 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/conftest.py +24 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_cli.py +36 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_config.py +87 -2
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/.gitignore +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/pyproject.toml +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/setup.cfg +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/setup.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/scm_file_list.json +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/browser.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/clipboard.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/container_runtime.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/docker_client.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/exceptions.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/viewer.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_clipboard.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_docker_client.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_tui.py +0 -0
- {nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/tests/test_viewer.py +0 -0
|
@@ -29,6 +29,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
29
29
|
|
|
30
30
|
### Fixed
|
|
31
31
|
|
|
32
|
+
- A `.ndslive` filestore, SmartLayer folder or service URL holding object
|
|
33
|
+
data is now configured as `SmartLayerObjectService` instead of always
|
|
34
|
+
`SmartLayerTileService`, which the viewer rejected at startup; detection
|
|
35
|
+
reads the filestore tables, an `objects/` folder, or the service's OpenAPI
|
|
36
|
+
spec (falls back to tile when the spec cannot be fetched)
|
|
32
37
|
- Clipboard copy in the Docker screen now uses the native OS clipboard
|
|
33
38
|
(`clip.exe` / `pbcopy` / `wl-copy`/`xclip`/`xsel`) instead of relying on the
|
|
34
39
|
OSC 52 terminal escape, which is unreliable on Windows; warns when a copy
|
{nds_mapviewer-2026.4.3.dev251/src/nds_mapviewer.egg-info → nds_mapviewer-2026.4.3.dev253}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.4.3.
|
|
3
|
+
Version: 2026.4.3.dev253
|
|
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
|
|
@@ -49,7 +49,7 @@ Full documentation: **https://developer.nds.live/tools/mapviewer**
|
|
|
49
49
|
- **Multiple data formats** — NDS.Live, GeoJSON, NDS.Classic (member edition)
|
|
50
50
|
- **Live style editing** — modify map styles in real time with YAML-based rules
|
|
51
51
|
- **Feature inspection** — click any map feature to explore its attributes and relations
|
|
52
|
-
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services
|
|
52
|
+
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services, as tile or object maps
|
|
53
53
|
- **Guided setup** — interactive wizard for creating data source configs
|
|
54
54
|
- **Config management** — save, switch, and delete configs from the RunScreen (`c`)
|
|
55
55
|
|
|
@@ -71,7 +71,7 @@ Either Docker or [Podman](https://podman.io) works as the container runtime —
|
|
|
71
71
|
```bash
|
|
72
72
|
mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml)
|
|
73
73
|
|
|
74
|
-
mapviewer /path/to/data.ndslive # View a local FileStore
|
|
74
|
+
mapviewer /path/to/data.ndslive # View a local FileStore (tile or object map)
|
|
75
75
|
mapviewer /path/to/folder # View a local SmartLayer folder
|
|
76
76
|
mapviewer /path/to/geojson/ # View a GeoJSON folder
|
|
77
77
|
mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
|
|
@@ -87,10 +87,19 @@ mapviewer --demo --network host # Run the container on the host network
|
|
|
87
87
|
|
|
88
88
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
89
89
|
|
|
90
|
+
### Tile and object maps
|
|
91
|
+
|
|
92
|
+
Local sources are detected as tile or object maps: a `.ndslive` filestore with an
|
|
93
|
+
`objectTable`, or a SmartLayer folder with an `objects/` directory, becomes a
|
|
94
|
+
`SmartLayerObjectService`; everything else a `SmartLayerTileService`. Object maps
|
|
95
|
+
need MapViewer image 2026.4.2 or newer.
|
|
96
|
+
|
|
90
97
|
### Remote service URLs
|
|
91
98
|
|
|
92
99
|
A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
|
|
93
|
-
which is turned into a `SmartLayerTileService
|
|
100
|
+
which is turned into a `SmartLayerTileService`, or a `SmartLayerObjectService` when its
|
|
101
|
+
OpenAPI spec offers `getObjectByReference`. If the spec cannot be fetched, the tile
|
|
102
|
+
service is assumed. The bare-URL form configures **no
|
|
94
103
|
authentication** — for protected services, proxy them through `ndslive-server` or
|
|
95
104
|
declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
|
|
96
105
|
bearer, OAuth2, proxy).
|
|
@@ -16,7 +16,7 @@ Full documentation: **https://developer.nds.live/tools/mapviewer**
|
|
|
16
16
|
- **Multiple data formats** — NDS.Live, GeoJSON, NDS.Classic (member edition)
|
|
17
17
|
- **Live style editing** — modify map styles in real time with YAML-based rules
|
|
18
18
|
- **Feature inspection** — click any map feature to explore its attributes and relations
|
|
19
|
-
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services
|
|
19
|
+
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services, as tile or object maps
|
|
20
20
|
- **Guided setup** — interactive wizard for creating data source configs
|
|
21
21
|
- **Config management** — save, switch, and delete configs from the RunScreen (`c`)
|
|
22
22
|
|
|
@@ -38,7 +38,7 @@ Either Docker or [Podman](https://podman.io) works as the container runtime —
|
|
|
38
38
|
```bash
|
|
39
39
|
mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml)
|
|
40
40
|
|
|
41
|
-
mapviewer /path/to/data.ndslive # View a local FileStore
|
|
41
|
+
mapviewer /path/to/data.ndslive # View a local FileStore (tile or object map)
|
|
42
42
|
mapviewer /path/to/folder # View a local SmartLayer folder
|
|
43
43
|
mapviewer /path/to/geojson/ # View a GeoJSON folder
|
|
44
44
|
mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
|
|
@@ -54,10 +54,19 @@ mapviewer --demo --network host # Run the container on the host network
|
|
|
54
54
|
|
|
55
55
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
56
56
|
|
|
57
|
+
### Tile and object maps
|
|
58
|
+
|
|
59
|
+
Local sources are detected as tile or object maps: a `.ndslive` filestore with an
|
|
60
|
+
`objectTable`, or a SmartLayer folder with an `objects/` directory, becomes a
|
|
61
|
+
`SmartLayerObjectService`; everything else a `SmartLayerTileService`. Object maps
|
|
62
|
+
need MapViewer image 2026.4.2 or newer.
|
|
63
|
+
|
|
57
64
|
### Remote service URLs
|
|
58
65
|
|
|
59
66
|
A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
|
|
60
|
-
which is turned into a `SmartLayerTileService
|
|
67
|
+
which is turned into a `SmartLayerTileService`, or a `SmartLayerObjectService` when its
|
|
68
|
+
OpenAPI spec offers `getObjectByReference`. If the spec cannot be fetched, the tile
|
|
69
|
+
service is assumed. The bare-URL form configures **no
|
|
61
70
|
authentication** — for protected services, proxy them through `ndslive-server` or
|
|
62
71
|
declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
|
|
63
72
|
bearer, OAuth2, proxy).
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253/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.4.3.
|
|
3
|
+
Version: 2026.4.3.dev253
|
|
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
|
|
@@ -49,7 +49,7 @@ Full documentation: **https://developer.nds.live/tools/mapviewer**
|
|
|
49
49
|
- **Multiple data formats** — NDS.Live, GeoJSON, NDS.Classic (member edition)
|
|
50
50
|
- **Live style editing** — modify map styles in real time with YAML-based rules
|
|
51
51
|
- **Feature inspection** — click any map feature to explore its attributes and relations
|
|
52
|
-
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services
|
|
52
|
+
- **Local and remote data** — view SmartLayer folders, FileStore files, or remote services, as tile or object maps
|
|
53
53
|
- **Guided setup** — interactive wizard for creating data source configs
|
|
54
54
|
- **Config management** — save, switch, and delete configs from the RunScreen (`c`)
|
|
55
55
|
|
|
@@ -71,7 +71,7 @@ Either Docker or [Podman](https://podman.io) works as the container runtime —
|
|
|
71
71
|
```bash
|
|
72
72
|
mapviewer # HomeScreen (or auto-loads ~/.nds/config.yaml)
|
|
73
73
|
|
|
74
|
-
mapviewer /path/to/data.ndslive # View a local FileStore
|
|
74
|
+
mapviewer /path/to/data.ndslive # View a local FileStore (tile or object map)
|
|
75
75
|
mapviewer /path/to/folder # View a local SmartLayer folder
|
|
76
76
|
mapviewer /path/to/geojson/ # View a GeoJSON folder
|
|
77
77
|
mapviewer https://host/openapi.json # Connect to a remote service (unauthenticated)
|
|
@@ -87,10 +87,19 @@ mapviewer --demo --network host # Run the container on the host network
|
|
|
87
87
|
|
|
88
88
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
89
89
|
|
|
90
|
+
### Tile and object maps
|
|
91
|
+
|
|
92
|
+
Local sources are detected as tile or object maps: a `.ndslive` filestore with an
|
|
93
|
+
`objectTable`, or a SmartLayer folder with an `objects/` directory, becomes a
|
|
94
|
+
`SmartLayerObjectService`; everything else a `SmartLayerTileService`. Object maps
|
|
95
|
+
need MapViewer image 2026.4.2 or newer.
|
|
96
|
+
|
|
90
97
|
### Remote service URLs
|
|
91
98
|
|
|
92
99
|
A source can be an http(s) service URL (e.g. `mapviewer https://my-host/openapi.json`),
|
|
93
|
-
which is turned into a `SmartLayerTileService
|
|
100
|
+
which is turned into a `SmartLayerTileService`, or a `SmartLayerObjectService` when its
|
|
101
|
+
OpenAPI spec offers `getObjectByReference`. If the spec cannot be fetched, the tile
|
|
102
|
+
service is assumed. The bare-URL form configures **no
|
|
94
103
|
authentication** — for protected services, proxy them through `ndslive-server` or
|
|
95
104
|
declare the service in a `config.yaml` with an `http-settings` block (api-key, basic,
|
|
96
105
|
bearer, OAuth2, proxy).
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/cli.py
RENAMED
|
@@ -118,7 +118,9 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
118
118
|
create_geojson_config,
|
|
119
119
|
create_classic_config,
|
|
120
120
|
create_service_config,
|
|
121
|
+
detect_service_type,
|
|
121
122
|
get_demo_config,
|
|
123
|
+
SMART_TILE_SERVICE,
|
|
122
124
|
)
|
|
123
125
|
from .tui import MapViewerApp
|
|
124
126
|
|
|
@@ -220,13 +222,21 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
220
222
|
source_config_path: Path | None = None
|
|
221
223
|
|
|
222
224
|
for source_arg in source_args:
|
|
223
|
-
# Remote service: an http(s) URL is turned into a
|
|
224
|
-
# source directly
|
|
225
|
-
#
|
|
226
|
-
#
|
|
225
|
+
# Remote service: an http(s) URL is turned into a SmartLayer tile or
|
|
226
|
+
# object service source directly, depending on its OpenAPI spec.
|
|
227
|
+
# Checked before the path:mapId split below because URLs contain
|
|
228
|
+
# colons. No auth is configured for the bare-URL form — use
|
|
229
|
+
# ndslive-server as a proxy or a config.yaml for authenticated
|
|
227
230
|
# services.
|
|
228
231
|
if source_arg.startswith(("http://", "https://")):
|
|
229
|
-
|
|
232
|
+
service_type = detect_service_type(source_arg)
|
|
233
|
+
if service_type is None:
|
|
234
|
+
service_type = SMART_TILE_SERVICE
|
|
235
|
+
console.print(
|
|
236
|
+
f"[yellow]Note:[/yellow] could not read the service spec of "
|
|
237
|
+
f"{source_arg}, assuming {SMART_TILE_SERVICE}"
|
|
238
|
+
)
|
|
239
|
+
config = create_service_config(source_arg, service_type=service_type)
|
|
230
240
|
descriptions.append(f"service: {source_arg}")
|
|
231
241
|
all_sources.extend(config.get("sources", []))
|
|
232
242
|
all_http_settings.extend(config.get("http-settings", []))
|
|
@@ -320,7 +330,7 @@ Examples:
|
|
|
320
330
|
# Interactive HomeScreen
|
|
321
331
|
mapviewer
|
|
322
332
|
|
|
323
|
-
# Quick start with local data
|
|
333
|
+
# Quick start with local data (tile or object maps are detected)
|
|
324
334
|
mapviewer /path/to/data.ndslive
|
|
325
335
|
mapviewer /path/to/smartlayer/folder
|
|
326
336
|
mapviewer /path/to/geojson/folder
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/config.py
RENAMED
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import sqlite3
|
|
7
8
|
import tempfile
|
|
9
|
+
import urllib.request
|
|
8
10
|
from dataclasses import dataclass, field
|
|
9
11
|
from pathlib import Path, PurePath
|
|
10
12
|
from typing import Any
|
|
@@ -29,6 +31,12 @@ CUSTOM_STYLES_ROOT = "/custom-styles"
|
|
|
29
31
|
CUSTOM_BACKGROUNDS_ROOT = "/custom-backgrounds"
|
|
30
32
|
COORDINATE_LEGAL_TERMS_ROOT = "/coordinates-legal-terms"
|
|
31
33
|
|
|
34
|
+
# NDS.Live SmartLayer datasource types; the viewer rejects a source whose
|
|
35
|
+
# type does not match the data's addressing mode.
|
|
36
|
+
SMART_TILE_SERVICE = "SmartLayerTileService"
|
|
37
|
+
SMART_OBJECT_SERVICE = "SmartLayerObjectService"
|
|
38
|
+
SERVICE_SPEC_TIMEOUT_S = 5
|
|
39
|
+
|
|
32
40
|
# Default config for demo mode
|
|
33
41
|
DEFAULT_DEMO_CONFIG = {
|
|
34
42
|
"http-settings": [
|
|
@@ -688,6 +696,61 @@ def detect_directory_type(path: Path) -> str:
|
|
|
688
696
|
return "folder"
|
|
689
697
|
|
|
690
698
|
|
|
699
|
+
# --- SmartLayer kind detection ---
|
|
700
|
+
|
|
701
|
+
def detect_filestore_type(filestore_path: str | Path) -> str:
|
|
702
|
+
"""Return the SmartLayer datasource type for a ``.ndslive`` filestore.
|
|
703
|
+
|
|
704
|
+
Object filestores carry an ``objectTable``, tile filestores a
|
|
705
|
+
``tileTable``. Unreadable files fall back to the tile type and leave the
|
|
706
|
+
error reporting to the viewer.
|
|
707
|
+
"""
|
|
708
|
+
uri = f"{Path(filestore_path).resolve().as_uri()}?mode=ro"
|
|
709
|
+
try:
|
|
710
|
+
conn = sqlite3.connect(uri, uri=True)
|
|
711
|
+
try:
|
|
712
|
+
row = conn.execute(
|
|
713
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='objectTable'"
|
|
714
|
+
).fetchone()
|
|
715
|
+
finally:
|
|
716
|
+
conn.close()
|
|
717
|
+
except sqlite3.Error:
|
|
718
|
+
return SMART_TILE_SERVICE
|
|
719
|
+
return SMART_OBJECT_SERVICE if row else SMART_TILE_SERVICE
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def detect_folder_type(folder_path: str | Path) -> str:
|
|
723
|
+
"""Return the SmartLayer datasource type for an unpacked SmartLayer folder.
|
|
724
|
+
|
|
725
|
+
Object folders keep their records in an ``objects/`` subdirectory.
|
|
726
|
+
"""
|
|
727
|
+
if (Path(folder_path) / "objects").is_dir():
|
|
728
|
+
return SMART_OBJECT_SERVICE
|
|
729
|
+
return SMART_TILE_SERVICE
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _fetch_service_spec(url: str) -> str | None:
|
|
733
|
+
"""Fetch a service's OpenAPI spec as text, or ``None`` if unreachable."""
|
|
734
|
+
try:
|
|
735
|
+
with urllib.request.urlopen(url, timeout=SERVICE_SPEC_TIMEOUT_S) as response:
|
|
736
|
+
return response.read().decode("utf-8", errors="replace")
|
|
737
|
+
except (OSError, ValueError):
|
|
738
|
+
return None
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def detect_service_type(url: str) -> str | None:
|
|
742
|
+
"""Return the SmartLayer datasource type for a remote service URL.
|
|
743
|
+
|
|
744
|
+
Object services expose ``getObjectByReference`` in their OpenAPI spec.
|
|
745
|
+
Returns ``None`` when the spec cannot be fetched (e.g. it requires auth),
|
|
746
|
+
so the caller can choose a fallback.
|
|
747
|
+
"""
|
|
748
|
+
spec = _fetch_service_spec(url)
|
|
749
|
+
if spec is None:
|
|
750
|
+
return None
|
|
751
|
+
return SMART_OBJECT_SERVICE if "getObjectByReference" in spec else SMART_TILE_SERVICE
|
|
752
|
+
|
|
753
|
+
|
|
691
754
|
# --- Config generation functions ---
|
|
692
755
|
|
|
693
756
|
def create_geojson_config(folder_path: str | Path) -> dict[str, Any]:
|
|
@@ -718,12 +781,12 @@ def create_classic_config(
|
|
|
718
781
|
def create_folder_config(
|
|
719
782
|
folder_path: str | Path, map_id: str | None = None,
|
|
720
783
|
) -> dict[str, Any]:
|
|
721
|
-
"""Create a config dict for a SmartLayer folder."""
|
|
784
|
+
"""Create a config dict for a SmartLayer folder (tile or object)."""
|
|
722
785
|
folder_path = Path(folder_path)
|
|
723
786
|
return {
|
|
724
787
|
"sources": [
|
|
725
788
|
{
|
|
726
|
-
"type":
|
|
789
|
+
"type": detect_folder_type(folder_path),
|
|
727
790
|
"uri": f"folder:{folder_path}",
|
|
728
791
|
"mapId": map_id or folder_path.name,
|
|
729
792
|
}
|
|
@@ -734,12 +797,12 @@ def create_folder_config(
|
|
|
734
797
|
def create_filestore_config(
|
|
735
798
|
filestore_path: str | Path, map_id: str | None = None,
|
|
736
799
|
) -> dict[str, Any]:
|
|
737
|
-
"""Create a config dict for a FileStore."""
|
|
800
|
+
"""Create a config dict for a FileStore (tile or object)."""
|
|
738
801
|
filestore_path = Path(filestore_path)
|
|
739
802
|
return {
|
|
740
803
|
"sources": [
|
|
741
804
|
{
|
|
742
|
-
"type":
|
|
805
|
+
"type": detect_filestore_type(filestore_path),
|
|
743
806
|
"uri": f"filestore:{filestore_path}",
|
|
744
807
|
"mapId": map_id or filestore_path.stem,
|
|
745
808
|
}
|
|
@@ -764,11 +827,13 @@ def create_service_config(
|
|
|
764
827
|
proxy_port: int | None = None,
|
|
765
828
|
proxy_user: str | None = None,
|
|
766
829
|
proxy_password: str | None = None,
|
|
830
|
+
service_type: str = SMART_TILE_SERVICE,
|
|
767
831
|
) -> dict[str, Any]:
|
|
768
832
|
"""Create a config dict for connecting to a remote service.
|
|
769
833
|
|
|
770
834
|
Args:
|
|
771
835
|
url: Service URL (e.g., https://api.example.com/openapi.json)
|
|
836
|
+
service_type: SmartLayer datasource type, see ``detect_service_type``
|
|
772
837
|
auth_method: One of "none", "apiKey", "queryParam", "basic", "bearer", "oauth2"
|
|
773
838
|
api_key: API key value (for auth_method="apiKey")
|
|
774
839
|
query_params: List of {"key": k, "value": v} dicts (for auth_method="queryParam")
|
|
@@ -794,7 +859,7 @@ def create_service_config(
|
|
|
794
859
|
config: dict[str, Any] = {
|
|
795
860
|
"sources": [
|
|
796
861
|
{
|
|
797
|
-
"type":
|
|
862
|
+
"type": service_type,
|
|
798
863
|
"uri": url,
|
|
799
864
|
"mapId": parsed.netloc.replace(".", "-"),
|
|
800
865
|
}
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/tui.py
RENAMED
|
@@ -33,6 +33,7 @@ from . import docker_client
|
|
|
33
33
|
from .config import (
|
|
34
34
|
CONFIGS_DIR,
|
|
35
35
|
DEFAULT_CONFIG,
|
|
36
|
+
SMART_TILE_SERVICE,
|
|
36
37
|
create_classic_config,
|
|
37
38
|
create_filestore_config,
|
|
38
39
|
create_folder_config,
|
|
@@ -40,6 +41,7 @@ from .config import (
|
|
|
40
41
|
create_service_config,
|
|
41
42
|
delete_config,
|
|
42
43
|
detect_directory_type,
|
|
44
|
+
detect_service_type,
|
|
43
45
|
get_demo_config,
|
|
44
46
|
list_saved_configs,
|
|
45
47
|
load_config,
|
|
@@ -712,6 +714,7 @@ class WizardScreen(Screen):
|
|
|
712
714
|
url=self._service_url,
|
|
713
715
|
**auth_kwargs,
|
|
714
716
|
**proxy_kwargs,
|
|
717
|
+
service_type=detect_service_type(self._service_url) or SMART_TILE_SERVICE,
|
|
715
718
|
)
|
|
716
719
|
elif self._source_type == "demo":
|
|
717
720
|
self._config = get_demo_config()
|
|
@@ -37,6 +37,30 @@ def edition():
|
|
|
37
37
|
return DEFAULT_EDITION
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
@pytest.fixture(autouse=True)
|
|
41
|
+
def offline_service_spec(monkeypatch):
|
|
42
|
+
"""Keep tests offline: service spec fetches fail unless a test stubs them."""
|
|
43
|
+
monkeypatch.setattr(
|
|
44
|
+
"ndslive.mapviewer.config._fetch_service_spec", lambda url: None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def make_filestore():
|
|
50
|
+
"""Factory for minimal SQLite ``.ndslive`` filestores with given tables."""
|
|
51
|
+
import sqlite3
|
|
52
|
+
|
|
53
|
+
def make(path, *tables):
|
|
54
|
+
conn = sqlite3.connect(path)
|
|
55
|
+
with conn:
|
|
56
|
+
for table in tables:
|
|
57
|
+
conn.execute(f"CREATE TABLE {table} (id INTEGER)")
|
|
58
|
+
conn.close()
|
|
59
|
+
return path
|
|
60
|
+
|
|
61
|
+
return make
|
|
62
|
+
|
|
63
|
+
|
|
40
64
|
@pytest.fixture
|
|
41
65
|
def temp_dir():
|
|
42
66
|
"""Create a temporary directory for test files."""
|
|
@@ -177,6 +177,42 @@ class TestCliUrlSource:
|
|
|
177
177
|
assert rc == 0
|
|
178
178
|
assert cap["config"]["sources"][0]["uri"] == url
|
|
179
179
|
|
|
180
|
+
def test_object_service_url(self, monkeypatch):
|
|
181
|
+
monkeypatch.setattr(
|
|
182
|
+
"ndslive.mapviewer.config._fetch_service_spec",
|
|
183
|
+
lambda url: '{"paths": {"/getObjectByReference": {}}}',
|
|
184
|
+
)
|
|
185
|
+
rc, cap = self._run_cmd_main(monkeypatch, ["https://my-host/openapi.json"])
|
|
186
|
+
assert rc == 0
|
|
187
|
+
assert cap["config"]["sources"][0]["type"] == "SmartLayerObjectService"
|
|
188
|
+
|
|
189
|
+
def test_unreadable_spec_falls_back_to_tile_with_note(self, monkeypatch, capsys):
|
|
190
|
+
rc, cap = self._run_cmd_main(monkeypatch, ["https://my-host/openapi.json"])
|
|
191
|
+
assert rc == 0
|
|
192
|
+
assert cap["config"]["sources"][0]["type"] == "SmartLayerTileService"
|
|
193
|
+
assert "could not read the service spec" in capsys.readouterr().out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestCliObjectSources:
|
|
197
|
+
"""Tests for detecting SmartLayer object sources from local paths."""
|
|
198
|
+
|
|
199
|
+
def test_mixed_tile_and_object_sources(self, monkeypatch, tmp_path, make_filestore):
|
|
200
|
+
tiles = make_filestore(tmp_path / "tiles.ndslive", "metadataTable", "tileTable")
|
|
201
|
+
objects = make_filestore(tmp_path / "objects.ndslive", "metadataTable", "objectTable")
|
|
202
|
+
folder = tmp_path / "objfolder"
|
|
203
|
+
(folder / "objects").mkdir(parents=True)
|
|
204
|
+
|
|
205
|
+
rc, cap = TestCliUrlSource()._run_cmd_main(
|
|
206
|
+
monkeypatch, [str(tiles), str(objects), str(folder)],
|
|
207
|
+
)
|
|
208
|
+
assert rc == 0
|
|
209
|
+
types = {s["mapId"]: s["type"] for s in cap["config"]["sources"]}
|
|
210
|
+
assert types == {
|
|
211
|
+
"tiles": "SmartLayerTileService",
|
|
212
|
+
"objects": "SmartLayerObjectService",
|
|
213
|
+
"objfolder": "SmartLayerObjectService",
|
|
214
|
+
}
|
|
215
|
+
|
|
180
216
|
|
|
181
217
|
class TestCliLegacyMigration:
|
|
182
218
|
"""Tests for legacy mapviewer.yaml migration hint."""
|
|
@@ -7,6 +7,8 @@ import yaml
|
|
|
7
7
|
|
|
8
8
|
from ndslive.mapviewer.config import (
|
|
9
9
|
CONFIGS_DIR,
|
|
10
|
+
SMART_OBJECT_SERVICE,
|
|
11
|
+
SMART_TILE_SERVICE,
|
|
10
12
|
_extract_local_path,
|
|
11
13
|
_is_localhost,
|
|
12
14
|
_join_container_path,
|
|
@@ -19,6 +21,9 @@ from ndslive.mapviewer.config import (
|
|
|
19
21
|
create_service_config,
|
|
20
22
|
delete_config,
|
|
21
23
|
detect_directory_type,
|
|
24
|
+
detect_filestore_type,
|
|
25
|
+
detect_folder_type,
|
|
26
|
+
detect_service_type,
|
|
22
27
|
list_saved_configs,
|
|
23
28
|
load_config,
|
|
24
29
|
resolve_config_source,
|
|
@@ -798,16 +803,29 @@ class TestCreateClassicConfig:
|
|
|
798
803
|
|
|
799
804
|
|
|
800
805
|
class TestCreateFolderConfig:
|
|
801
|
-
"""Tests for create_folder_config()
|
|
806
|
+
"""Tests for create_folder_config()."""
|
|
802
807
|
|
|
803
808
|
def test_custom_map_id(self, temp_dir):
|
|
804
809
|
"""Custom mapId overrides the default."""
|
|
805
810
|
config = create_folder_config(temp_dir, map_id="CustomFolder")
|
|
806
811
|
assert config["sources"][0]["mapId"] == "CustomFolder"
|
|
807
812
|
|
|
813
|
+
def test_tile_folder(self, temp_dir):
|
|
814
|
+
"""A folder without objects/ is a tile service."""
|
|
815
|
+
(temp_dir / "tiles").mkdir()
|
|
816
|
+
config = create_folder_config(temp_dir)
|
|
817
|
+
assert config["sources"][0]["type"] == SMART_TILE_SERVICE
|
|
818
|
+
|
|
819
|
+
def test_object_folder(self, temp_dir):
|
|
820
|
+
"""A folder with objects/ is an object service."""
|
|
821
|
+
(temp_dir / "objects").mkdir()
|
|
822
|
+
config = create_folder_config(temp_dir)
|
|
823
|
+
assert config["sources"][0]["type"] == SMART_OBJECT_SERVICE
|
|
824
|
+
assert config["sources"][0]["uri"] == f"folder:{temp_dir}"
|
|
825
|
+
|
|
808
826
|
|
|
809
827
|
class TestCreateFilestoreConfig:
|
|
810
|
-
"""Tests for create_filestore_config()
|
|
828
|
+
"""Tests for create_filestore_config()."""
|
|
811
829
|
|
|
812
830
|
def test_custom_map_id(self, temp_dir):
|
|
813
831
|
"""Custom mapId overrides the default."""
|
|
@@ -816,6 +834,66 @@ class TestCreateFilestoreConfig:
|
|
|
816
834
|
config = create_filestore_config(store, map_id="CustomStore")
|
|
817
835
|
assert config["sources"][0]["mapId"] == "CustomStore"
|
|
818
836
|
|
|
837
|
+
def test_tile_filestore(self, temp_dir, make_filestore):
|
|
838
|
+
"""A filestore with a tileTable is a tile service."""
|
|
839
|
+
store = make_filestore(temp_dir / "tiles.ndslive", "metadataTable", "tileTable")
|
|
840
|
+
config = create_filestore_config(store)
|
|
841
|
+
assert config["sources"][0]["type"] == SMART_TILE_SERVICE
|
|
842
|
+
|
|
843
|
+
def test_object_filestore(self, temp_dir, make_filestore):
|
|
844
|
+
"""A filestore with an objectTable is an object service."""
|
|
845
|
+
store = make_filestore(
|
|
846
|
+
temp_dir / "objects.ndslive",
|
|
847
|
+
"metadataTable", "objectTable", "tileObjectAssociationTable",
|
|
848
|
+
)
|
|
849
|
+
config = create_filestore_config(store)
|
|
850
|
+
source = config["sources"][0]
|
|
851
|
+
assert source["type"] == SMART_OBJECT_SERVICE
|
|
852
|
+
assert source["uri"] == f"filestore:{store}"
|
|
853
|
+
assert source["mapId"] == "objects"
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class TestDetectSmartLayerType:
|
|
857
|
+
"""Tests for the SmartLayer tile/object detection helpers."""
|
|
858
|
+
|
|
859
|
+
def test_unreadable_filestore_falls_back_to_tile(self, temp_dir):
|
|
860
|
+
"""A file that is not SQLite is left to the viewer to reject."""
|
|
861
|
+
store = temp_dir / "broken.ndslive"
|
|
862
|
+
store.write_text("not a database")
|
|
863
|
+
assert detect_filestore_type(store) == SMART_TILE_SERVICE
|
|
864
|
+
|
|
865
|
+
def test_filestore_detection_leaves_file_unchanged(self, temp_dir, make_filestore):
|
|
866
|
+
"""Detection opens the filestore read-only."""
|
|
867
|
+
store = make_filestore(temp_dir / "objects.ndslive", "objectTable")
|
|
868
|
+
before = store.read_bytes()
|
|
869
|
+
assert detect_filestore_type(store) == SMART_OBJECT_SERVICE
|
|
870
|
+
assert store.read_bytes() == before
|
|
871
|
+
|
|
872
|
+
def test_folder_with_objects_file_is_tile(self, temp_dir):
|
|
873
|
+
"""Only an objects/ directory marks an object folder."""
|
|
874
|
+
(temp_dir / "objects").touch()
|
|
875
|
+
assert detect_folder_type(temp_dir) == SMART_TILE_SERVICE
|
|
876
|
+
|
|
877
|
+
def test_object_service_spec(self, monkeypatch):
|
|
878
|
+
"""A spec with getObjectByReference is an object service."""
|
|
879
|
+
monkeypatch.setattr(
|
|
880
|
+
"ndslive.mapviewer.config._fetch_service_spec",
|
|
881
|
+
lambda url: '{"paths": {"/getObjectByReference": {}}}',
|
|
882
|
+
)
|
|
883
|
+
assert detect_service_type("https://h/openapi.json") == SMART_OBJECT_SERVICE
|
|
884
|
+
|
|
885
|
+
def test_tile_service_spec(self, monkeypatch):
|
|
886
|
+
"""A spec without object methods is a tile service."""
|
|
887
|
+
monkeypatch.setattr(
|
|
888
|
+
"ndslive.mapviewer.config._fetch_service_spec",
|
|
889
|
+
lambda url: '{"paths": {"/getSmartLayerTile": {}}}',
|
|
890
|
+
)
|
|
891
|
+
assert detect_service_type("https://h/openapi.json") == SMART_TILE_SERVICE
|
|
892
|
+
|
|
893
|
+
def test_unreachable_spec_is_unknown(self):
|
|
894
|
+
"""An unreachable spec leaves the choice to the caller."""
|
|
895
|
+
assert detect_service_type("https://h/openapi.json") is None
|
|
896
|
+
|
|
819
897
|
|
|
820
898
|
class TestResolveConfigSourceDetection:
|
|
821
899
|
"""Tests for resolve_config_source() directory type detection."""
|
|
@@ -852,6 +930,13 @@ class TestCreateServiceConfig:
|
|
|
852
930
|
assert config["sources"][0]["uri"] == "https://api.example.com/openapi.json"
|
|
853
931
|
assert "http-settings" not in config
|
|
854
932
|
|
|
933
|
+
def test_object_service_type(self):
|
|
934
|
+
"""The given service type is written to the source."""
|
|
935
|
+
config = create_service_config(
|
|
936
|
+
"https://api.example.com/openapi.json", service_type=SMART_OBJECT_SERVICE,
|
|
937
|
+
)
|
|
938
|
+
assert config["sources"][0]["type"] == SMART_OBJECT_SERVICE
|
|
939
|
+
|
|
855
940
|
def test_api_key_auth(self):
|
|
856
941
|
"""API key auth populates http-settings with api-key."""
|
|
857
942
|
config = create_service_config(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/__init__.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/browser.py
RENAMED
|
File without changes
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/clipboard.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{nds_mapviewer-2026.4.3.dev251 → nds_mapviewer-2026.4.3.dev253}/src/ndslive/mapviewer/viewer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|