nds-mapviewer 2026.2.1.dev92__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.
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/CHANGELOG.md +24 -0
- {nds_mapviewer-2026.2.1.dev92/src/nds_mapviewer.egg-info → nds_mapviewer-2026.2.1.dev94}/PKG-INFO +39 -1
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/README.md +38 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94/src/nds_mapviewer.egg-info}/PKG-INFO +39 -1
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/SOURCES.txt +3 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/cli.py +82 -1
- nds_mapviewer-2026.2.1.dev94/src/ndslive/mapviewer/clipboard.py +81 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/config.py +31 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/docker_client.py +36 -14
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/tui.py +110 -13
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/viewer.py +43 -4
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/tests/test_cli.py +103 -0
- nds_mapviewer-2026.2.1.dev94/tests/test_clipboard.py +76 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/tests/test_config.py +71 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/tests/test_docker_client.py +57 -0
- nds_mapviewer-2026.2.1.dev94/tests/test_tui.py +67 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/tests/test_viewer.py +179 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/.gitignore +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/pyproject.toml +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/setup.cfg +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/setup.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/browser.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/container_runtime.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/exceptions.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/tests/conftest.py +0 -0
|
@@ -9,12 +9,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- `--debug` flag to enable container debug logging (sets `HTTP_LOG_LEVEL=debug`)
|
|
13
|
+
- `--env KEY=VALUE` (repeatable, short `-E`) to pass custom environment
|
|
14
|
+
variables into the container; overrides `--debug` on key collision
|
|
15
|
+
- `--network NAME` to attach the container to a specific network (e.g. `host`);
|
|
16
|
+
in `host` mode port publishing is skipped and the viewer is reached on the
|
|
17
|
+
container's internal port
|
|
18
|
+
- Run screen now shows a config line below the info banner with the source
|
|
19
|
+
(config file or directly-passed data-source path, e.g. a `*.ndslive`
|
|
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
|
|
12
26
|
- Default Docker image version pinned to package (`--image-version` defaults to
|
|
13
27
|
matching calver instead of `"latest"`)
|
|
14
28
|
- SCM-based versioning — package version derived from MapViewer git tags
|
|
15
29
|
|
|
16
30
|
### Fixed
|
|
17
31
|
|
|
32
|
+
- Clipboard copy in the Docker screen now uses the native OS clipboard
|
|
33
|
+
(`clip.exe` / `pbcopy` / `wl-copy`/`xclip`/`xsel`) instead of relying on the
|
|
34
|
+
OSC 52 terminal escape, which is unreliable on Windows; warns when a copy
|
|
35
|
+
cannot be confirmed
|
|
36
|
+
- Quitting the TUI no longer hangs — the log-follow stream is torn down by
|
|
37
|
+
terminating the process before closing pipes, and container shutdown is
|
|
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`
|
|
18
42
|
- Container readiness check uses log sentinel before HTTP polling to avoid
|
|
19
43
|
startup crash (mapget#151)
|
|
20
44
|
|
{nds_mapviewer-2026.2.1.dev92/src/nds_mapviewer.egg-info → nds_mapviewer-2026.2.1.dev94}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.2.1.
|
|
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,14 +74,48 @@ 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
|
|
80
81
|
mapviewer /path/a.ndslive /path/b # Multiple sources at once
|
|
82
|
+
|
|
83
|
+
mapviewer --demo --debug # Enable debug logging in the container
|
|
84
|
+
mapviewer --demo --env KEY=VALUE # Set a custom container env var (repeatable)
|
|
85
|
+
mapviewer --demo --network host # Run the container on the host network
|
|
81
86
|
```
|
|
82
87
|
|
|
83
88
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
84
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
|
+
|
|
103
|
+
### Container environment variables
|
|
104
|
+
|
|
105
|
+
`--debug` turns on verbose logging in the MapViewer container (shorthand for
|
|
106
|
+
`--env HTTP_LOG_LEVEL=debug`). Use `--env KEY=VALUE` (repeatable, short form
|
|
107
|
+
`-E`) to pass arbitrary environment variables into the container; explicit
|
|
108
|
+
`--env` entries override `--debug` on key collision. The resulting `-e` flags
|
|
109
|
+
appear in the Docker screen's "Copy Command" output.
|
|
110
|
+
|
|
111
|
+
### Container network
|
|
112
|
+
|
|
113
|
+
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
114
|
+
bridge or `host`). With `--network host` the container binds the host's port
|
|
115
|
+
directly, so `--port`/`--bind` are ignored and the MapViewer is reached on its
|
|
116
|
+
internal port (8089). Host networking behaves differently across platforms and
|
|
117
|
+
runtimes — it is intended as a connectivity workaround.
|
|
118
|
+
|
|
85
119
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
86
120
|
automatically and goes straight to the RunScreen.
|
|
87
121
|
|
|
@@ -89,6 +123,10 @@ If you edit `~/.nds/config.yaml` manually, use host paths there, not container
|
|
|
89
123
|
paths. The CLI translates them into the correct container mounts when it starts
|
|
90
124
|
the container runtime.
|
|
91
125
|
|
|
126
|
+
The run screen shows, below the info banner, the source you passed (a config
|
|
127
|
+
file or a directly-given data-source path such as a `*.ndslive` filestore) and
|
|
128
|
+
the mapped config that was generated and mounted into the container.
|
|
129
|
+
|
|
92
130
|
## Editions
|
|
93
131
|
|
|
94
132
|
| Edition | Formats | Access |
|
|
@@ -41,14 +41,48 @@ 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
|
|
47
48
|
mapviewer /path/a.ndslive /path/b # Multiple sources at once
|
|
49
|
+
|
|
50
|
+
mapviewer --demo --debug # Enable debug logging in the container
|
|
51
|
+
mapviewer --demo --env KEY=VALUE # Set a custom container env var (repeatable)
|
|
52
|
+
mapviewer --demo --network host # Run the container on the host network
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
51
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
|
+
|
|
70
|
+
### Container environment variables
|
|
71
|
+
|
|
72
|
+
`--debug` turns on verbose logging in the MapViewer container (shorthand for
|
|
73
|
+
`--env HTTP_LOG_LEVEL=debug`). Use `--env KEY=VALUE` (repeatable, short form
|
|
74
|
+
`-E`) to pass arbitrary environment variables into the container; explicit
|
|
75
|
+
`--env` entries override `--debug` on key collision. The resulting `-e` flags
|
|
76
|
+
appear in the Docker screen's "Copy Command" output.
|
|
77
|
+
|
|
78
|
+
### Container network
|
|
79
|
+
|
|
80
|
+
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
81
|
+
bridge or `host`). With `--network host` the container binds the host's port
|
|
82
|
+
directly, so `--port`/`--bind` are ignored and the MapViewer is reached on its
|
|
83
|
+
internal port (8089). Host networking behaves differently across platforms and
|
|
84
|
+
runtimes — it is intended as a connectivity workaround.
|
|
85
|
+
|
|
52
86
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
53
87
|
automatically and goes straight to the RunScreen.
|
|
54
88
|
|
|
@@ -56,6 +90,10 @@ If you edit `~/.nds/config.yaml` manually, use host paths there, not container
|
|
|
56
90
|
paths. The CLI translates them into the correct container mounts when it starts
|
|
57
91
|
the container runtime.
|
|
58
92
|
|
|
93
|
+
The run screen shows, below the info banner, the source you passed (a config
|
|
94
|
+
file or a directly-given data-source path such as a `*.ndslive` filestore) and
|
|
95
|
+
the mapped config that was generated and mounted into the container.
|
|
96
|
+
|
|
59
97
|
## Editions
|
|
60
98
|
|
|
61
99
|
| Edition | Formats | Access |
|
{nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94/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.2.1.
|
|
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,14 +74,48 @@ 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
|
|
80
81
|
mapviewer /path/a.ndslive /path/b # Multiple sources at once
|
|
82
|
+
|
|
83
|
+
mapviewer --demo --debug # Enable debug logging in the container
|
|
84
|
+
mapviewer --demo --env KEY=VALUE # Set a custom container env var (repeatable)
|
|
85
|
+
mapviewer --demo --network host # Run the container on the host network
|
|
81
86
|
```
|
|
82
87
|
|
|
83
88
|
All commands accept `-e community|member` and `-i VERSION`.
|
|
84
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
|
+
|
|
103
|
+
### Container environment variables
|
|
104
|
+
|
|
105
|
+
`--debug` turns on verbose logging in the MapViewer container (shorthand for
|
|
106
|
+
`--env HTTP_LOG_LEVEL=debug`). Use `--env KEY=VALUE` (repeatable, short form
|
|
107
|
+
`-E`) to pass arbitrary environment variables into the container; explicit
|
|
108
|
+
`--env` entries override `--debug` on key collision. The resulting `-e` flags
|
|
109
|
+
appear in the Docker screen's "Copy Command" output.
|
|
110
|
+
|
|
111
|
+
### Container network
|
|
112
|
+
|
|
113
|
+
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
114
|
+
bridge or `host`). With `--network host` the container binds the host's port
|
|
115
|
+
directly, so `--port`/`--bind` are ignored and the MapViewer is reached on its
|
|
116
|
+
internal port (8089). Host networking behaves differently across platforms and
|
|
117
|
+
runtimes — it is intended as a connectivity workaround.
|
|
118
|
+
|
|
85
119
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
86
120
|
automatically and goes straight to the RunScreen.
|
|
87
121
|
|
|
@@ -89,6 +123,10 @@ If you edit `~/.nds/config.yaml` manually, use host paths there, not container
|
|
|
89
123
|
paths. The CLI translates them into the correct container mounts when it starts
|
|
90
124
|
the container runtime.
|
|
91
125
|
|
|
126
|
+
The run screen shows, below the info banner, the source you passed (a config
|
|
127
|
+
file or a directly-given data-source path such as a `*.ndslive` filestore) and
|
|
128
|
+
the mapped config that was generated and mounted into the container.
|
|
129
|
+
|
|
92
130
|
## Editions
|
|
93
131
|
|
|
94
132
|
| Edition | Formats | Access |
|
{nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/nds_mapviewer.egg-info/SOURCES.txt
RENAMED
|
@@ -12,6 +12,7 @@ src/nds_mapviewer.egg-info/top_level.txt
|
|
|
12
12
|
src/ndslive/mapviewer/__init__.py
|
|
13
13
|
src/ndslive/mapviewer/browser.py
|
|
14
14
|
src/ndslive/mapviewer/cli.py
|
|
15
|
+
src/ndslive/mapviewer/clipboard.py
|
|
15
16
|
src/ndslive/mapviewer/config.py
|
|
16
17
|
src/ndslive/mapviewer/container_runtime.py
|
|
17
18
|
src/ndslive/mapviewer/docker_client.py
|
|
@@ -21,6 +22,8 @@ src/ndslive/mapviewer/ui.py
|
|
|
21
22
|
src/ndslive/mapviewer/viewer.py
|
|
22
23
|
tests/conftest.py
|
|
23
24
|
tests/test_cli.py
|
|
25
|
+
tests/test_clipboard.py
|
|
24
26
|
tests/test_config.py
|
|
25
27
|
tests/test_docker_client.py
|
|
28
|
+
tests/test_tui.py
|
|
26
29
|
tests/test_viewer.py
|
|
@@ -73,6 +73,37 @@ def _get_pull_policy_default() -> str:
|
|
|
73
73
|
return "prompt"
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _build_environment(args: argparse.Namespace) -> dict[str, str]:
|
|
77
|
+
"""Build the container environment dict from ``--debug`` and ``--env``.
|
|
78
|
+
|
|
79
|
+
``--debug`` is shorthand for ``HTTP_LOG_LEVEL=debug``. Explicit ``--env``
|
|
80
|
+
entries are applied afterwards, so they win on key collision.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
SystemExit: If an ``--env`` entry is not in ``KEY=VALUE`` form.
|
|
84
|
+
"""
|
|
85
|
+
env: dict[str, str] = {}
|
|
86
|
+
if getattr(args, "debug", False):
|
|
87
|
+
env["HTTP_LOG_LEVEL"] = "debug"
|
|
88
|
+
for item in getattr(args, "env", None) or []:
|
|
89
|
+
if "=" not in item:
|
|
90
|
+
console.print(
|
|
91
|
+
f"[red]Error:[/red] Invalid --env value '{item}', "
|
|
92
|
+
"expected KEY=VALUE"
|
|
93
|
+
)
|
|
94
|
+
raise SystemExit(2)
|
|
95
|
+
key, _, value = item.partition("=")
|
|
96
|
+
key = key.strip()
|
|
97
|
+
if not key:
|
|
98
|
+
console.print(
|
|
99
|
+
f"[red]Error:[/red] Invalid --env value '{item}', "
|
|
100
|
+
"empty key"
|
|
101
|
+
)
|
|
102
|
+
raise SystemExit(2)
|
|
103
|
+
env[key] = value
|
|
104
|
+
return env
|
|
105
|
+
|
|
106
|
+
|
|
76
107
|
# ---------------------------------------------------------------------------
|
|
77
108
|
# Command handler
|
|
78
109
|
# ---------------------------------------------------------------------------
|
|
@@ -86,6 +117,7 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
86
117
|
create_folder_config,
|
|
87
118
|
create_geojson_config,
|
|
88
119
|
create_classic_config,
|
|
120
|
+
create_service_config,
|
|
89
121
|
get_demo_config,
|
|
90
122
|
)
|
|
91
123
|
from .tui import MapViewerApp
|
|
@@ -97,6 +129,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
97
129
|
bind_address = args.bind
|
|
98
130
|
pull_policy = args.pull_policy
|
|
99
131
|
source_args = args.sources
|
|
132
|
+
environment = _build_environment(args)
|
|
133
|
+
network = getattr(args, "network", None)
|
|
100
134
|
|
|
101
135
|
# --demo → demo config
|
|
102
136
|
if args.demo:
|
|
@@ -108,6 +142,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
108
142
|
port=port,
|
|
109
143
|
bind_address=bind_address,
|
|
110
144
|
pull_policy=pull_policy,
|
|
145
|
+
environment=environment,
|
|
146
|
+
network=network,
|
|
111
147
|
run_config=config,
|
|
112
148
|
run_label="Demo (NDS Islands)",
|
|
113
149
|
is_demo=True,
|
|
@@ -128,6 +164,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
128
164
|
port=port,
|
|
129
165
|
bind_address=bind_address,
|
|
130
166
|
pull_policy=pull_policy,
|
|
167
|
+
environment=environment,
|
|
168
|
+
network=network,
|
|
131
169
|
run_config=config,
|
|
132
170
|
run_config_path=DEFAULT_CONFIG,
|
|
133
171
|
run_label="~/.nds/config.yaml",
|
|
@@ -151,6 +189,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
151
189
|
port=port,
|
|
152
190
|
bind_address=bind_address,
|
|
153
191
|
pull_policy=pull_policy,
|
|
192
|
+
environment=environment,
|
|
193
|
+
network=network,
|
|
154
194
|
run_config=config,
|
|
155
195
|
run_config_path=LEGACY_CONFIG,
|
|
156
196
|
run_label="~/.nds/mapviewer.yaml",
|
|
@@ -167,6 +207,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
167
207
|
port=port,
|
|
168
208
|
bind_address=bind_address,
|
|
169
209
|
pull_policy=pull_policy,
|
|
210
|
+
environment=environment,
|
|
211
|
+
network=network,
|
|
170
212
|
)
|
|
171
213
|
app.run()
|
|
172
214
|
return 0
|
|
@@ -178,6 +220,18 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
178
220
|
source_config_path: Path | None = None
|
|
179
221
|
|
|
180
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
|
+
|
|
181
235
|
# Parse optional path:mapId syntax
|
|
182
236
|
custom_map_id = None
|
|
183
237
|
if ":" in source_arg:
|
|
@@ -240,9 +294,12 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
240
294
|
port=port,
|
|
241
295
|
bind_address=bind_address,
|
|
242
296
|
pull_policy=pull_policy,
|
|
297
|
+
environment=environment,
|
|
298
|
+
network=network,
|
|
243
299
|
run_config=merged,
|
|
244
300
|
run_config_path=source_config_path,
|
|
245
301
|
run_label=source_desc,
|
|
302
|
+
run_source_paths=source_args,
|
|
246
303
|
)
|
|
247
304
|
app.run()
|
|
248
305
|
return 0
|
|
@@ -269,6 +326,9 @@ Examples:
|
|
|
269
326
|
mapviewer /path/to/geojson/folder
|
|
270
327
|
mapviewer --demo
|
|
271
328
|
|
|
329
|
+
# Connect to a remote service (unauthenticated)
|
|
330
|
+
mapviewer https://my-host/openapi.json
|
|
331
|
+
|
|
272
332
|
# Multiple sources at once
|
|
273
333
|
mapviewer /path/a.ndslive /path/to/geojson/
|
|
274
334
|
|
|
@@ -291,7 +351,7 @@ Examples:
|
|
|
291
351
|
parser.add_argument(
|
|
292
352
|
"sources",
|
|
293
353
|
nargs="*",
|
|
294
|
-
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",
|
|
295
355
|
)
|
|
296
356
|
parser.add_argument(
|
|
297
357
|
"--demo",
|
|
@@ -323,6 +383,27 @@ Examples:
|
|
|
323
383
|
"--bind", default="127.0.0.1",
|
|
324
384
|
help="Host address to bind (default: 127.0.0.1, use 0.0.0.0 for network access)",
|
|
325
385
|
)
|
|
386
|
+
parser.add_argument(
|
|
387
|
+
"--env", "-E",
|
|
388
|
+
action="append",
|
|
389
|
+
metavar="KEY=VALUE",
|
|
390
|
+
dest="env",
|
|
391
|
+
default=[],
|
|
392
|
+
help="Set a container environment variable (repeatable), "
|
|
393
|
+
"e.g. --env HTTP_LOG_LEVEL=debug",
|
|
394
|
+
)
|
|
395
|
+
parser.add_argument(
|
|
396
|
+
"--debug",
|
|
397
|
+
action="store_true",
|
|
398
|
+
help="Enable debug logging in the container (sets HTTP_LOG_LEVEL=debug)",
|
|
399
|
+
)
|
|
400
|
+
parser.add_argument(
|
|
401
|
+
"--network",
|
|
402
|
+
metavar="NAME",
|
|
403
|
+
default=None,
|
|
404
|
+
help="Container network to attach (e.g. 'host'). With 'host' the "
|
|
405
|
+
"MapViewer binds the host port directly and --port/--bind are ignored.",
|
|
406
|
+
)
|
|
326
407
|
|
|
327
408
|
pull_group = parser.add_mutually_exclusive_group()
|
|
328
409
|
pull_group.add_argument(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Cross-platform clipboard helper.
|
|
2
|
+
|
|
3
|
+
Textual's ``App.copy_to_clipboard`` relies on the OSC 52 terminal escape
|
|
4
|
+
sequence, which many Windows terminals (conhost, some Windows Terminal
|
|
5
|
+
configs, PowerShell hosts) silently drop — so the copy buttons appear to do
|
|
6
|
+
nothing. This module copies via the native OS clipboard utility first and
|
|
7
|
+
only falls back to OSC 52 when no native tool is available.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import platform
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run_copy(cmd: list[str], text: str) -> bool:
|
|
18
|
+
"""Pipe *text* into *cmd* on stdin; return True on success."""
|
|
19
|
+
try:
|
|
20
|
+
proc = subprocess.run(
|
|
21
|
+
cmd,
|
|
22
|
+
input=text,
|
|
23
|
+
text=True,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
timeout=5,
|
|
26
|
+
)
|
|
27
|
+
return proc.returncode == 0
|
|
28
|
+
except (OSError, subprocess.SubprocessError):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _native_copy(text: str) -> bool:
|
|
33
|
+
"""Copy *text* using the platform-native clipboard tool.
|
|
34
|
+
|
|
35
|
+
Returns True if a tool was found and reported success.
|
|
36
|
+
"""
|
|
37
|
+
system = platform.system()
|
|
38
|
+
|
|
39
|
+
if system == "Windows":
|
|
40
|
+
# clip.exe ships with every supported Windows version.
|
|
41
|
+
clip = shutil.which("clip") or shutil.which("clip.exe")
|
|
42
|
+
if clip:
|
|
43
|
+
return _run_copy([clip], text)
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
if system == "Darwin":
|
|
47
|
+
if shutil.which("pbcopy"):
|
|
48
|
+
return _run_copy(["pbcopy"], text)
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# Linux / other Unix — try Wayland then X11 helpers.
|
|
52
|
+
if shutil.which("wl-copy"):
|
|
53
|
+
if _run_copy(["wl-copy"], text):
|
|
54
|
+
return True
|
|
55
|
+
if shutil.which("xclip"):
|
|
56
|
+
if _run_copy(["xclip", "-selection", "clipboard"], text):
|
|
57
|
+
return True
|
|
58
|
+
if shutil.which("xsel"):
|
|
59
|
+
if _run_copy(["xsel", "--clipboard", "--input"], text):
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def copy_text(text: str, app=None) -> bool:
|
|
65
|
+
"""Copy *text* to the clipboard, returning True on confirmed success.
|
|
66
|
+
|
|
67
|
+
Tries the native OS clipboard utility first (reliable on Windows where
|
|
68
|
+
OSC 52 is commonly unsupported). If no native tool is available and an
|
|
69
|
+
*app* is given, falls back to Textual's OSC 52 escape — which cannot be
|
|
70
|
+
confirmed, so this returns False to let callers warn the user.
|
|
71
|
+
"""
|
|
72
|
+
if _native_copy(text):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
if app is not None:
|
|
76
|
+
try:
|
|
77
|
+
app.copy_to_clipboard(text)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return False
|
{nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/config.py
RENAMED
|
@@ -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
|
|
{nds_mapviewer-2026.2.1.dev92 → nds_mapviewer-2026.2.1.dev94}/src/ndslive/mapviewer/docker_client.py
RENAMED
|
@@ -36,6 +36,9 @@ IMAGE_NAME = "nds-mapviewer"
|
|
|
36
36
|
|
|
37
37
|
KNOWN_TAGS = ["latest", "dev", "dev-rc"]
|
|
38
38
|
|
|
39
|
+
# Port the MapViewer HTTP server listens on inside the container.
|
|
40
|
+
CONTAINER_PORT = 8089
|
|
41
|
+
|
|
39
42
|
|
|
40
43
|
# ---------------------------------------------------------------------------
|
|
41
44
|
# Container info dataclass (replaces Docker SDK Container object)
|
|
@@ -543,6 +546,7 @@ def run_container(
|
|
|
543
546
|
config_dir: str | Path | None = None,
|
|
544
547
|
volumes: list[str] | None = None,
|
|
545
548
|
environment: dict[str, str] | None = None,
|
|
549
|
+
network: str | None = None,
|
|
546
550
|
name: str = CONTAINER_NAME,
|
|
547
551
|
detach: bool = True,
|
|
548
552
|
remove_existing: bool = False,
|
|
@@ -556,6 +560,9 @@ def run_container(
|
|
|
556
560
|
config_dir: Config directory to mount at ``/config``.
|
|
557
561
|
volumes: Additional volume mounts as ``"host:container:mode"`` strings.
|
|
558
562
|
environment: Environment variables.
|
|
563
|
+
network: Container network to attach (e.g. ``"host"``). When ``"host"``,
|
|
564
|
+
port publishing is skipped because the container binds the host's
|
|
565
|
+
port directly.
|
|
559
566
|
name: Container name.
|
|
560
567
|
detach: Run in detached mode.
|
|
561
568
|
remove_existing: Remove existing container if running.
|
|
@@ -571,12 +578,20 @@ def run_container(
|
|
|
571
578
|
rt.run("stop", "-t", "2", name, timeout=30)
|
|
572
579
|
rt.run("rm", name, check=False, timeout=10)
|
|
573
580
|
|
|
581
|
+
use_host_net = network == "host"
|
|
582
|
+
|
|
574
583
|
# Build command
|
|
575
584
|
cmd: list[str] = ["run"]
|
|
576
585
|
if detach:
|
|
577
586
|
cmd.append("-d")
|
|
578
587
|
cmd.extend(["--name", name])
|
|
579
|
-
|
|
588
|
+
if network:
|
|
589
|
+
cmd.extend(["--network", network])
|
|
590
|
+
# Host networking ignores published ports — the container binds the
|
|
591
|
+
# host's port directly — so skip the -p mapping to avoid the runtime
|
|
592
|
+
# warning/error.
|
|
593
|
+
if not use_host_net:
|
|
594
|
+
cmd.extend(["-p", f"{bind_address}:{port}:{CONTAINER_PORT}"])
|
|
580
595
|
|
|
581
596
|
# Volume mounts
|
|
582
597
|
if config_dir:
|
|
@@ -592,8 +607,10 @@ def run_container(
|
|
|
592
607
|
for key, value in environment.items():
|
|
593
608
|
cmd.extend(["-e", f"{key}={value}"])
|
|
594
609
|
|
|
595
|
-
# Extra hosts for host.docker.internal on Linux
|
|
596
|
-
|
|
610
|
+
# Extra hosts for host.docker.internal on Linux. The host-gateway mapping
|
|
611
|
+
# conflicts with host networking, so skip it in that mode.
|
|
612
|
+
if not use_host_net:
|
|
613
|
+
cmd.extend(rt.get_extra_host_args())
|
|
597
614
|
|
|
598
615
|
# Labels
|
|
599
616
|
cmd.extend(["--label", "app=ndslive-mapviewer"])
|
|
@@ -673,6 +690,22 @@ class ContainerLogStream:
|
|
|
673
690
|
def close(self, timeout: float = 3.0) -> None:
|
|
674
691
|
if self._proc.poll() is not None:
|
|
675
692
|
return
|
|
693
|
+
# Terminate the process FIRST so the OS closes the write end of the
|
|
694
|
+
# pipe and any thread blocked in `for line in proc.stdout` unblocks
|
|
695
|
+
# via EOF. Closing the pipe from another thread while it is being
|
|
696
|
+
# read is unsafe in CPython and can leave the reader (and thus app
|
|
697
|
+
# shutdown) hung indefinitely.
|
|
698
|
+
self._proc.terminate()
|
|
699
|
+
try:
|
|
700
|
+
self._proc.wait(timeout=timeout)
|
|
701
|
+
except subprocess.TimeoutExpired:
|
|
702
|
+
self._proc.kill()
|
|
703
|
+
try:
|
|
704
|
+
self._proc.wait(timeout=timeout)
|
|
705
|
+
except subprocess.TimeoutExpired:
|
|
706
|
+
pass
|
|
707
|
+
# Now that the process is gone, closing the pipes is safe and frees
|
|
708
|
+
# the file descriptors.
|
|
676
709
|
for pipe in (getattr(self._proc, "stdout", None), getattr(self._proc, "stderr", None)):
|
|
677
710
|
if pipe is None:
|
|
678
711
|
continue
|
|
@@ -680,17 +713,6 @@ class ContainerLogStream:
|
|
|
680
713
|
pipe.close()
|
|
681
714
|
except Exception:
|
|
682
715
|
pass
|
|
683
|
-
self._proc.terminate()
|
|
684
|
-
try:
|
|
685
|
-
self._proc.wait(timeout=timeout)
|
|
686
|
-
return
|
|
687
|
-
except subprocess.TimeoutExpired:
|
|
688
|
-
pass
|
|
689
|
-
self._proc.kill()
|
|
690
|
-
try:
|
|
691
|
-
self._proc.wait(timeout=timeout)
|
|
692
|
-
except subprocess.TimeoutExpired:
|
|
693
|
-
pass
|
|
694
716
|
|
|
695
717
|
|
|
696
718
|
_READY_SENTINEL = "NDS MapViewer is ready!"
|