nds-mapviewer 2026.3.2.dev115__tar.gz → 2026.3.2.dev117__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.3.2.dev115/src/nds_mapviewer.egg-info → nds_mapviewer-2026.3.2.dev117}/PKG-INFO +1 -1
  2. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117/src/nds_mapviewer.egg-info}/PKG-INFO +1 -1
  3. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/config.py +85 -0
  4. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_config.py +140 -0
  5. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/.gitignore +0 -0
  6. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/CHANGELOG.md +0 -0
  7. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/README.md +0 -0
  8. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/pyproject.toml +0 -0
  9. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/setup.cfg +0 -0
  10. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/setup.py +0 -0
  11. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
  12. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
  13. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
  14. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/nds_mapviewer.egg-info/requires.txt +0 -0
  15. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
  16. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/__init__.py +0 -0
  17. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/browser.py +0 -0
  18. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/cli.py +0 -0
  19. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/clipboard.py +0 -0
  20. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/container_runtime.py +0 -0
  21. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/docker_client.py +0 -0
  22. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/exceptions.py +0 -0
  23. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/tui.py +0 -0
  24. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/ui.py +0 -0
  25. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/src/ndslive/mapviewer/viewer.py +0 -0
  26. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/conftest.py +0 -0
  27. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_cli.py +0 -0
  28. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_clipboard.py +0 -0
  29. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_docker_client.py +0 -0
  30. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_tui.py +0 -0
  31. {nds_mapviewer-2026.3.2.dev115 → nds_mapviewer-2026.3.2.dev117}/tests/test_viewer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.3.2.dev115
3
+ Version: 2026.3.2.dev117
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nds-mapviewer
3
- Version: 2026.3.2.dev115
3
+ Version: 2026.3.2.dev117
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
@@ -26,7 +26,9 @@ FILESTORE_PATTERN = re.compile(r"^filestore:(.+)$")
26
26
  FOLDER_URI_PATTERN = re.compile(r"^folder:(.+)$")
27
27
  LOCALHOST_PATTERN = re.compile(r"^(localhost|127\.0\.0\.1)(:\d+)?$")
28
28
  CUSTOM_STYLES_ROOT = "/custom-styles"
29
+ STYLE_OPTION_PRESETS_ROOT = "/style-option-presets"
29
30
  CUSTOM_BACKGROUNDS_ROOT = "/custom-backgrounds"
31
+ COORDINATE_LEGAL_TERMS_ROOT = "/coordinates-legal-terms"
30
32
 
31
33
  # Default config for demo mode
32
34
  DEFAULT_DEMO_CONFIG = {
@@ -478,6 +480,34 @@ def transform_config(
478
480
  return container_dir
479
481
  return f"{container_dir}/{suffix}"
480
482
 
483
+ def transform_coordinate_legal_terms(value: str) -> str:
484
+ """Mount one local legal-terms document below its public static root."""
485
+ value = value.strip()
486
+ if not value or _is_explicit_browser_path(value, COORDINATE_LEGAL_TERMS_ROOT):
487
+ return value
488
+ host_file = _resolve_host_asset_path(value, base_dir)
489
+ return add_asset_volume(
490
+ host_file,
491
+ COORDINATE_LEGAL_TERMS_ROOT,
492
+ f"{COORDINATE_LEGAL_TERMS_ROOT}/{host_file.name}",
493
+ )
494
+
495
+ def transform_style_option_presets(value: str) -> str:
496
+ """Mount one local preset document below its dedicated public root."""
497
+ value = value.strip()
498
+ if (
499
+ not value
500
+ or _is_external_or_bundle_url(value)
501
+ or _is_explicit_browser_path(value, STYLE_OPTION_PRESETS_ROOT)
502
+ ):
503
+ return value
504
+ host_file = _resolve_host_asset_path(value, base_dir)
505
+ return add_asset_volume(
506
+ host_file,
507
+ STYLE_OPTION_PRESETS_ROOT,
508
+ f"{STYLE_OPTION_PRESETS_ROOT}/{host_file.name}",
509
+ )
510
+
481
511
  erdblick = transformed.get("erdblick")
482
512
  if isinstance(erdblick, dict):
483
513
  additional_styles = erdblick.get("additionalStyles")
@@ -503,6 +533,18 @@ def transform_config(
503
533
  for item in url_template
504
534
  ]
505
535
 
536
+ coordinate_legal_terms = erdblick.get("coordinates-legal-terms")
537
+ if isinstance(coordinate_legal_terms, str):
538
+ erdblick["coordinates-legal-terms"] = transform_coordinate_legal_terms(
539
+ coordinate_legal_terms
540
+ )
541
+
542
+ style_option_presets = erdblick.get("styleOptionPresets")
543
+ if isinstance(style_option_presets, str):
544
+ erdblick["styleOptionPresets"] = transform_style_option_presets(
545
+ style_option_presets
546
+ )
547
+
506
548
  return TransformedConfig(config=transformed, volumes=volumes + asset_volumes)
507
549
 
508
550
 
@@ -625,6 +667,49 @@ def validate_paths(
625
667
  f"Directory does not exist: {host_root}"
626
668
  )
627
669
 
670
+ coordinate_legal_terms = erdblick.get("coordinates-legal-terms")
671
+ if isinstance(coordinate_legal_terms, str):
672
+ coordinate_legal_terms = coordinate_legal_terms.strip()
673
+ if (
674
+ coordinate_legal_terms
675
+ and not _is_explicit_browser_path(
676
+ coordinate_legal_terms, COORDINATE_LEGAL_TERMS_ROOT
677
+ )
678
+ ):
679
+ host_file = _resolve_host_asset_path(coordinate_legal_terms, base_dir)
680
+ if host_file.suffix.lower() not in (".json", ".yaml", ".yml"):
681
+ errors.append(
682
+ "erdblick.coordinates-legal-terms: "
683
+ "File must use .json, .yaml, or .yml"
684
+ )
685
+ elif not host_file.is_file():
686
+ errors.append(
687
+ "erdblick.coordinates-legal-terms: "
688
+ f"File does not exist: {host_file}"
689
+ )
690
+
691
+ style_option_presets = erdblick.get("styleOptionPresets")
692
+ if isinstance(style_option_presets, str):
693
+ style_option_presets = style_option_presets.strip()
694
+ if (
695
+ style_option_presets
696
+ and not _is_external_or_bundle_url(style_option_presets)
697
+ and not _is_explicit_browser_path(
698
+ style_option_presets, STYLE_OPTION_PRESETS_ROOT
699
+ )
700
+ ):
701
+ host_file = _resolve_host_asset_path(style_option_presets, base_dir)
702
+ if host_file.suffix.lower() not in (".yaml", ".yml"):
703
+ errors.append(
704
+ "erdblick.styleOptionPresets: "
705
+ "File must use .yaml or .yml"
706
+ )
707
+ elif not host_file.is_file():
708
+ errors.append(
709
+ "erdblick.styleOptionPresets: "
710
+ f"File does not exist: {host_file}"
711
+ )
712
+
628
713
  return errors
629
714
 
630
715
 
@@ -453,6 +453,90 @@ class TestTransformConfig:
453
453
  assert layers[2]["url"] == "service"
454
454
  assert any(_same_path(vol.host_path, imagery_dir) for vol in result.volumes)
455
455
 
456
+ def test_erdblick_coordinate_legal_terms_are_mapped_from_config_dir(
457
+ self, temp_dir
458
+ ):
459
+ """Relative coordinate terms are mounted at the dedicated browser root."""
460
+ legal_dir = temp_dir / "legal"
461
+ legal_dir.mkdir()
462
+ legal_file = legal_dir / "coordinates.yaml"
463
+ legal_file.write_text("legal-terms: Test terms\n")
464
+ config = {
465
+ "sources": [],
466
+ "erdblick": {
467
+ "coordinates-enabled": True,
468
+ "coordinates-legal-terms": "legal/coordinates.yaml",
469
+ },
470
+ }
471
+
472
+ result = transform_config(config, base_dir=temp_dir)
473
+
474
+ assert result.config["erdblick"]["coordinates-enabled"] is True
475
+ assert result.config["erdblick"]["coordinates-legal-terms"] == (
476
+ "/coordinates-legal-terms/coordinates.yaml"
477
+ )
478
+ assert any(
479
+ _same_path(vol.host_path, legal_file)
480
+ and vol.container_path == "/coordinates-legal-terms/coordinates.yaml"
481
+ for vol in result.volumes
482
+ )
483
+
484
+ def test_explicit_coordinate_legal_terms_browser_path_is_preserved(self):
485
+ """Raw-Docker browser paths do not create Python-managed mounts."""
486
+ config = {
487
+ "sources": [],
488
+ "erdblick": {
489
+ "coordinates-legal-terms": (
490
+ "/coordinates-legal-terms/manual.json"
491
+ )
492
+ },
493
+ }
494
+
495
+ result = transform_config(config)
496
+
497
+ assert result.config["erdblick"]["coordinates-legal-terms"] == (
498
+ "/coordinates-legal-terms/manual.json"
499
+ )
500
+ assert result.volumes == []
501
+
502
+ def test_style_option_presets_are_mapped_from_config_dir(self, temp_dir):
503
+ """A relative preset YAML is mounted as one file at its dedicated root."""
504
+ presets_dir = temp_dir / "presets"
505
+ presets_dir.mkdir()
506
+ presets_file = presets_dir / "viewer.yaml"
507
+ presets_file.write_text("version: 1\npresets: []\n")
508
+ config = {
509
+ "sources": [],
510
+ "erdblick": {
511
+ "styleOptionPresets": "presets/viewer.yaml",
512
+ },
513
+ }
514
+
515
+ result = transform_config(config, base_dir=temp_dir)
516
+
517
+ assert result.config["erdblick"]["styleOptionPresets"] == (
518
+ "/style-option-presets/viewer.yaml"
519
+ )
520
+ assert any(
521
+ _same_path(volume.host_path, presets_file)
522
+ and volume.container_path == "/style-option-presets/viewer.yaml"
523
+ for volume in result.volumes
524
+ )
525
+
526
+ def test_style_option_preset_urls_are_preserved(self):
527
+ """External, bundled, and explicit browser URLs do not create mounts."""
528
+ for value in (
529
+ "https://example.com/presets.yaml",
530
+ "bundle/styles/style-option-presets.yaml",
531
+ "/style-option-presets/manual.yaml",
532
+ ):
533
+ result = transform_config({
534
+ "sources": [],
535
+ "erdblick": {"styleOptionPresets": value},
536
+ })
537
+ assert result.config["erdblick"]["styleOptionPresets"] == value
538
+ assert result.volumes == []
539
+
456
540
  def test_existing_volume_mapping_is_reused(self, temp_dir):
457
541
  """Hot reloads keep container paths stable when entries are removed."""
458
542
  first = temp_dir / "first"
@@ -614,6 +698,62 @@ class TestValidatePaths:
614
698
  assert "erdblick.additionalStyles[0]" in errors[0]
615
699
  assert "erdblick.backgroundLayers[0].urlTemplate[0]" in errors[1]
616
700
 
701
+ def test_coordinate_legal_terms_path_and_extension_are_validated(self, temp_dir):
702
+ """Coordinate terms must be an existing JSON/YAML file."""
703
+ legal_file = temp_dir / "terms.json"
704
+ legal_file.write_text('{"legal-terms":"Text"}\n')
705
+
706
+ assert validate_paths(
707
+ {"erdblick": {"coordinates-legal-terms": "terms.json"}},
708
+ base_dir=temp_dir,
709
+ ) == []
710
+
711
+ missing_errors = validate_paths(
712
+ {"erdblick": {"coordinates-legal-terms": "missing.yaml"}},
713
+ base_dir=temp_dir,
714
+ )
715
+ extension_errors = validate_paths(
716
+ {"erdblick": {"coordinates-legal-terms": "terms.txt"}},
717
+ base_dir=temp_dir,
718
+ )
719
+
720
+ assert len(missing_errors) == 1
721
+ assert "File does not exist" in missing_errors[0]
722
+ assert len(extension_errors) == 1
723
+ assert "File must use .json, .yaml, or .yml" in extension_errors[0]
724
+
725
+ def test_style_option_preset_path_and_extension_are_validated(self, temp_dir):
726
+ """Preset configuration must point at an existing YAML file."""
727
+ presets_file = temp_dir / "presets.yaml"
728
+ presets_file.write_text("version: 1\npresets: []\n")
729
+
730
+ assert validate_paths(
731
+ {"erdblick": {"styleOptionPresets": "presets.yaml"}},
732
+ base_dir=temp_dir,
733
+ ) == []
734
+ assert validate_paths(
735
+ {"erdblick": {"styleOptionPresets": "https://example.com/presets.yaml"}},
736
+ base_dir=temp_dir,
737
+ ) == []
738
+ assert validate_paths(
739
+ {"erdblick": {"styleOptionPresets": "/style-option-presets/manual.yaml"}},
740
+ base_dir=temp_dir,
741
+ ) == []
742
+
743
+ missing_errors = validate_paths(
744
+ {"erdblick": {"styleOptionPresets": "missing.yaml"}},
745
+ base_dir=temp_dir,
746
+ )
747
+ extension_errors = validate_paths(
748
+ {"erdblick": {"styleOptionPresets": "presets.json"}},
749
+ base_dir=temp_dir,
750
+ )
751
+
752
+ assert len(missing_errors) == 1
753
+ assert "File does not exist" in missing_errors[0]
754
+ assert len(extension_errors) == 1
755
+ assert "File must use .yaml or .yml" in extension_errors[0]
756
+
617
757
 
618
758
  class TestWriteConfigToTemp:
619
759
  """Tests for write_config_to_temp()."""