gitmap-core 0.1.0__py3-none-any.whl
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.
- gitmap_core/README.md +46 -0
- gitmap_core/__init__.py +100 -0
- gitmap_core/communication.py +346 -0
- gitmap_core/compat.py +408 -0
- gitmap_core/connection.py +232 -0
- gitmap_core/context.py +709 -0
- gitmap_core/diff.py +283 -0
- gitmap_core/maps.py +385 -0
- gitmap_core/merge.py +449 -0
- gitmap_core/models.py +332 -0
- gitmap_core/py.typed +0 -0
- gitmap_core/pyproject.toml +48 -0
- gitmap_core/remote.py +728 -0
- gitmap_core/repository.py +1632 -0
- gitmap_core/tests/__init__.py +1 -0
- gitmap_core/tests/test_communication.py +695 -0
- gitmap_core/tests/test_compat.py +310 -0
- gitmap_core/tests/test_connection.py +314 -0
- gitmap_core/tests/test_context.py +814 -0
- gitmap_core/tests/test_diff.py +567 -0
- gitmap_core/tests/test_init.py +153 -0
- gitmap_core/tests/test_maps.py +642 -0
- gitmap_core/tests/test_merge.py +694 -0
- gitmap_core/tests/test_models.py +410 -0
- gitmap_core/tests/test_remote.py +3014 -0
- gitmap_core/tests/test_repository.py +1639 -0
- gitmap_core/tests/test_visualize.py +902 -0
- gitmap_core/visualize.py +1217 -0
- gitmap_core-0.1.0.dist-info/METADATA +961 -0
- gitmap_core-0.1.0.dist-info/RECORD +32 -0
- gitmap_core-0.1.0.dist-info/WHEEL +4 -0
- gitmap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
gitmap_core/diff.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""JSON diffing and comparison module.
|
|
2
|
+
|
|
3
|
+
Provides utilities for comparing web map JSON structures,
|
|
4
|
+
detecting changes at the layer and property level.
|
|
5
|
+
|
|
6
|
+
Execution Context:
|
|
7
|
+
Library module - imported by CLI diff and merge commands
|
|
8
|
+
|
|
9
|
+
Dependencies:
|
|
10
|
+
- deepdiff: Deep dictionary comparison
|
|
11
|
+
|
|
12
|
+
Metadata:
|
|
13
|
+
Version: 0.1.0
|
|
14
|
+
Author: GitMap Team
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from dataclasses import field
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from deepdiff import DeepDiff
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---- Data Classes -------------------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LayerChange:
|
|
30
|
+
"""Represents a change to a single layer.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
layer_id: ID of the changed layer.
|
|
34
|
+
layer_title: Title of the layer.
|
|
35
|
+
change_type: Type of change (added, removed, modified).
|
|
36
|
+
details: Detailed change information.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
layer_id: str
|
|
40
|
+
layer_title: str
|
|
41
|
+
change_type: str # 'added', 'removed', 'modified'
|
|
42
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class MapDiff:
|
|
47
|
+
"""Represents differences between two map states.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
layer_changes: List of layer-level changes.
|
|
51
|
+
table_changes: List of table-level changes.
|
|
52
|
+
property_changes: Map-level property changes.
|
|
53
|
+
has_changes: Whether any changes were detected.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
layer_changes: list[LayerChange] = field(default_factory=list)
|
|
57
|
+
table_changes: list[LayerChange] = field(default_factory=list)
|
|
58
|
+
property_changes: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def has_changes(
|
|
62
|
+
self,
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""Check if any changes exist."""
|
|
65
|
+
return bool(self.layer_changes or self.table_changes or self.property_changes)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def added_layers(
|
|
69
|
+
self,
|
|
70
|
+
) -> list[LayerChange]:
|
|
71
|
+
"""Get added layers."""
|
|
72
|
+
return [c for c in self.layer_changes if c.change_type == "added"]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def removed_layers(
|
|
76
|
+
self,
|
|
77
|
+
) -> list[LayerChange]:
|
|
78
|
+
"""Get removed layers."""
|
|
79
|
+
return [c for c in self.layer_changes if c.change_type == "removed"]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def modified_layers(
|
|
83
|
+
self,
|
|
84
|
+
) -> list[LayerChange]:
|
|
85
|
+
"""Get modified layers."""
|
|
86
|
+
return [c for c in self.layer_changes if c.change_type == "modified"]
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def added_tables(
|
|
90
|
+
self,
|
|
91
|
+
) -> list[LayerChange]:
|
|
92
|
+
"""Get added tables."""
|
|
93
|
+
return [c for c in self.table_changes if c.change_type == "added"]
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def removed_tables(
|
|
97
|
+
self,
|
|
98
|
+
) -> list[LayerChange]:
|
|
99
|
+
"""Get removed tables."""
|
|
100
|
+
return [c for c in self.table_changes if c.change_type == "removed"]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def modified_tables(
|
|
104
|
+
self,
|
|
105
|
+
) -> list[LayerChange]:
|
|
106
|
+
"""Get modified tables."""
|
|
107
|
+
return [c for c in self.table_changes if c.change_type == "modified"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---- Diff Functions -----------------------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def diff_maps(
|
|
114
|
+
map1: dict[str, Any],
|
|
115
|
+
map2: dict[str, Any],
|
|
116
|
+
) -> MapDiff:
|
|
117
|
+
"""Compare two web map JSON structures.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
map1: First map (typically current/index state).
|
|
121
|
+
map2: Second map (typically committed state).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
MapDiff object describing all differences.
|
|
125
|
+
"""
|
|
126
|
+
result = MapDiff()
|
|
127
|
+
|
|
128
|
+
# Compare operational layers
|
|
129
|
+
layers1 = map1.get("operationalLayers", [])
|
|
130
|
+
layers2 = map2.get("operationalLayers", [])
|
|
131
|
+
|
|
132
|
+
layer_changes = diff_layers(layers1, layers2)
|
|
133
|
+
result.layer_changes = layer_changes
|
|
134
|
+
|
|
135
|
+
# Compare tables (same structure as layers)
|
|
136
|
+
tables1 = map1.get("tables", [])
|
|
137
|
+
tables2 = map2.get("tables", [])
|
|
138
|
+
|
|
139
|
+
table_changes = diff_layers(tables1, tables2)
|
|
140
|
+
result.table_changes = table_changes
|
|
141
|
+
|
|
142
|
+
# Compare map-level properties (excluding layers and tables)
|
|
143
|
+
map1_props = {k: v for k, v in map1.items() if k not in ("operationalLayers", "tables")}
|
|
144
|
+
map2_props = {k: v for k, v in map2.items() if k not in ("operationalLayers", "tables")}
|
|
145
|
+
|
|
146
|
+
if map1_props != map2_props:
|
|
147
|
+
deep_diff = DeepDiff(map2_props, map1_props, ignore_order=True)
|
|
148
|
+
result.property_changes = deep_diff.to_dict() if deep_diff else {}
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def diff_layers(
|
|
154
|
+
layers1: list[dict[str, Any]],
|
|
155
|
+
layers2: list[dict[str, Any]],
|
|
156
|
+
) -> list[LayerChange]:
|
|
157
|
+
"""Compare two lists of operational layers.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
layers1: First layer list (current state).
|
|
161
|
+
layers2: Second layer list (previous state).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of LayerChange objects.
|
|
165
|
+
"""
|
|
166
|
+
changes = []
|
|
167
|
+
|
|
168
|
+
# Index layers by ID
|
|
169
|
+
index1 = {layer.get("id"): layer for layer in layers1 if layer.get("id")}
|
|
170
|
+
index2 = {layer.get("id"): layer for layer in layers2 if layer.get("id")}
|
|
171
|
+
|
|
172
|
+
# Find added layers (in layers1 but not layers2)
|
|
173
|
+
for layer_id, layer in index1.items():
|
|
174
|
+
if layer_id not in index2:
|
|
175
|
+
changes.append(LayerChange(
|
|
176
|
+
layer_id=str(layer_id),
|
|
177
|
+
layer_title=layer.get("title", "Untitled"),
|
|
178
|
+
change_type="added",
|
|
179
|
+
))
|
|
180
|
+
|
|
181
|
+
# Find removed layers (in layers2 but not layers1)
|
|
182
|
+
for layer_id, layer in index2.items():
|
|
183
|
+
if layer_id not in index1:
|
|
184
|
+
changes.append(LayerChange(
|
|
185
|
+
layer_id=str(layer_id),
|
|
186
|
+
layer_title=layer.get("title", "Untitled"),
|
|
187
|
+
change_type="removed",
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
# Find modified layers (in both but different)
|
|
191
|
+
for layer_id in index1:
|
|
192
|
+
if layer_id in index2:
|
|
193
|
+
layer1 = index1[layer_id]
|
|
194
|
+
layer2 = index2[layer_id]
|
|
195
|
+
|
|
196
|
+
if layer1 != layer2:
|
|
197
|
+
deep_diff = DeepDiff(layer2, layer1, ignore_order=True)
|
|
198
|
+
changes.append(LayerChange(
|
|
199
|
+
layer_id=str(layer_id),
|
|
200
|
+
layer_title=layer1.get("title", "Untitled"),
|
|
201
|
+
change_type="modified",
|
|
202
|
+
details=deep_diff.to_dict() if deep_diff else {},
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
return changes
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def diff_json(
|
|
209
|
+
obj1: Any,
|
|
210
|
+
obj2: Any,
|
|
211
|
+
ignore_order: bool = True,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Generic JSON diff using DeepDiff.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
obj1: First object (current state).
|
|
217
|
+
obj2: Second object (previous state).
|
|
218
|
+
ignore_order: Whether to ignore list ordering.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary of differences.
|
|
222
|
+
"""
|
|
223
|
+
deep_diff = DeepDiff(obj2, obj1, ignore_order=ignore_order)
|
|
224
|
+
return deep_diff.to_dict() if deep_diff else {}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---- Formatting Functions -----------------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def format_diff_summary(
|
|
231
|
+
map_diff: MapDiff,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Format MapDiff as human-readable summary.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
map_diff: MapDiff object.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Formatted string summary.
|
|
240
|
+
"""
|
|
241
|
+
if not map_diff.has_changes:
|
|
242
|
+
return "No changes detected."
|
|
243
|
+
|
|
244
|
+
lines = []
|
|
245
|
+
|
|
246
|
+
if map_diff.added_layers:
|
|
247
|
+
lines.append(f"Added layers ({len(map_diff.added_layers)}):")
|
|
248
|
+
for change in map_diff.added_layers:
|
|
249
|
+
lines.append(f" + {change.layer_title} ({change.layer_id})")
|
|
250
|
+
|
|
251
|
+
if map_diff.removed_layers:
|
|
252
|
+
lines.append(f"Removed layers ({len(map_diff.removed_layers)}):")
|
|
253
|
+
for change in map_diff.removed_layers:
|
|
254
|
+
lines.append(f" - {change.layer_title} ({change.layer_id})")
|
|
255
|
+
|
|
256
|
+
if map_diff.modified_layers:
|
|
257
|
+
lines.append(f"Modified layers ({len(map_diff.modified_layers)}):")
|
|
258
|
+
for change in map_diff.modified_layers:
|
|
259
|
+
lines.append(f" ~ {change.layer_title} ({change.layer_id})")
|
|
260
|
+
|
|
261
|
+
if map_diff.added_tables:
|
|
262
|
+
lines.append(f"Added tables ({len(map_diff.added_tables)}):")
|
|
263
|
+
for change in map_diff.added_tables:
|
|
264
|
+
lines.append(f" + {change.layer_title} ({change.layer_id})")
|
|
265
|
+
|
|
266
|
+
if map_diff.removed_tables:
|
|
267
|
+
lines.append(f"Removed tables ({len(map_diff.removed_tables)}):")
|
|
268
|
+
for change in map_diff.removed_tables:
|
|
269
|
+
lines.append(f" - {change.layer_title} ({change.layer_id})")
|
|
270
|
+
|
|
271
|
+
if map_diff.modified_tables:
|
|
272
|
+
lines.append(f"Modified tables ({len(map_diff.modified_tables)}):")
|
|
273
|
+
for change in map_diff.modified_tables:
|
|
274
|
+
lines.append(f" ~ {change.layer_title} ({change.layer_id})")
|
|
275
|
+
|
|
276
|
+
if map_diff.property_changes:
|
|
277
|
+
lines.append("Map properties changed:")
|
|
278
|
+
for key in map_diff.property_changes:
|
|
279
|
+
lines.append(f" * {key}")
|
|
280
|
+
|
|
281
|
+
return "\n".join(lines)
|
|
282
|
+
|
|
283
|
+
|
gitmap_core/maps.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Web map JSON operations module.
|
|
2
|
+
|
|
3
|
+
Handles extraction, serialization, and staging of ArcGIS web map JSON
|
|
4
|
+
data for version control operations.
|
|
5
|
+
|
|
6
|
+
Execution Context:
|
|
7
|
+
Library module - imported by CLI commands and remote operations
|
|
8
|
+
|
|
9
|
+
Dependencies:
|
|
10
|
+
- arcgis: Web map item access
|
|
11
|
+
- gitmap_core.models: Data models
|
|
12
|
+
|
|
13
|
+
Metadata:
|
|
14
|
+
Version: 0.1.0
|
|
15
|
+
Author: GitMap Team
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from arcgis.gis import GIS
|
|
26
|
+
from arcgis.gis import Item
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---- Map Extraction Functions -------------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_webmap_json(
|
|
33
|
+
item: Item,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Extract web map JSON from Portal item.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
item: ArcGIS web map item.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Web map JSON as dictionary.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
RuntimeError: If item is not a web map or extraction fails.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
if item.type != "Web Map":
|
|
48
|
+
msg = f"Item '{item.title}' is not a Web Map (type: {item.type})"
|
|
49
|
+
raise RuntimeError(msg)
|
|
50
|
+
|
|
51
|
+
# Get the web map data
|
|
52
|
+
webmap_data = item.get_data()
|
|
53
|
+
|
|
54
|
+
if not webmap_data:
|
|
55
|
+
msg = f"Failed to get data from web map '{item.title}'"
|
|
56
|
+
raise RuntimeError(msg)
|
|
57
|
+
|
|
58
|
+
return webmap_data
|
|
59
|
+
|
|
60
|
+
except Exception as extraction_error:
|
|
61
|
+
if isinstance(extraction_error, RuntimeError):
|
|
62
|
+
raise
|
|
63
|
+
msg = f"Failed to extract web map JSON: {extraction_error}"
|
|
64
|
+
raise RuntimeError(msg) from extraction_error
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_webmap_by_id(
|
|
68
|
+
gis: GIS,
|
|
69
|
+
item_id: str,
|
|
70
|
+
) -> tuple[Item, dict[str, Any]]:
|
|
71
|
+
"""Fetch web map item and its JSON by item ID.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
gis: Authenticated GIS connection.
|
|
75
|
+
item_id: Portal item ID.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (Item, web map JSON dict).
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
RuntimeError: If item not found or is not a web map.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
item = gis.content.get(item_id)
|
|
85
|
+
|
|
86
|
+
if not item:
|
|
87
|
+
msg = f"Item with ID '{item_id}' not found"
|
|
88
|
+
raise RuntimeError(msg)
|
|
89
|
+
|
|
90
|
+
webmap_json = get_webmap_json(item)
|
|
91
|
+
return item, webmap_json
|
|
92
|
+
|
|
93
|
+
except Exception as fetch_error:
|
|
94
|
+
if isinstance(fetch_error, RuntimeError):
|
|
95
|
+
raise
|
|
96
|
+
msg = f"Failed to fetch web map {item_id}: {fetch_error}"
|
|
97
|
+
raise RuntimeError(msg) from fetch_error
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def list_webmaps(
|
|
101
|
+
gis: GIS,
|
|
102
|
+
query: str = "",
|
|
103
|
+
owner: str = "",
|
|
104
|
+
tag: str = "",
|
|
105
|
+
max_results: int = 100,
|
|
106
|
+
) -> list[dict[str, str]]:
|
|
107
|
+
"""List available web maps from the Portal.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
gis: Authenticated GIS connection.
|
|
111
|
+
query: Optional search query string to filter web maps (e.g., "title:MyMap").
|
|
112
|
+
Defaults to empty string to list all web maps.
|
|
113
|
+
owner: Optional owner username to filter web maps.
|
|
114
|
+
tag: Optional tag to filter web maps.
|
|
115
|
+
max_results: Maximum number of web maps to return (default: 100).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of dictionaries containing web map information (id, title, owner, type).
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
RuntimeError: If web map search fails.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
# Build search query components
|
|
125
|
+
query_parts = ['type:"Web Map"']
|
|
126
|
+
|
|
127
|
+
if owner:
|
|
128
|
+
query_parts.append(f'owner:{owner}')
|
|
129
|
+
|
|
130
|
+
if tag:
|
|
131
|
+
query_parts.append(f'tags:{tag}')
|
|
132
|
+
|
|
133
|
+
if query:
|
|
134
|
+
query_parts.append(query)
|
|
135
|
+
|
|
136
|
+
# Combine all query parts with AND
|
|
137
|
+
search_query = ' AND '.join(query_parts)
|
|
138
|
+
items = gis.content.search(query=search_query, max_items=max_results)
|
|
139
|
+
|
|
140
|
+
result = []
|
|
141
|
+
for item in items:
|
|
142
|
+
result.append({
|
|
143
|
+
"id": getattr(item, "id", ""),
|
|
144
|
+
"title": getattr(item, "title", ""),
|
|
145
|
+
"owner": getattr(item, "owner", ""),
|
|
146
|
+
"type": getattr(item, "type", ""),
|
|
147
|
+
})
|
|
148
|
+
return result
|
|
149
|
+
except Exception as search_error:
|
|
150
|
+
msg = f"Failed to search web maps: {search_error}"
|
|
151
|
+
raise RuntimeError(msg) from search_error
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def list_services(
|
|
155
|
+
gis: GIS,
|
|
156
|
+
query: str = "",
|
|
157
|
+
owner: str = "",
|
|
158
|
+
service_type: str = "",
|
|
159
|
+
max_results: int = 100,
|
|
160
|
+
) -> list[dict[str, str]]:
|
|
161
|
+
"""List available services (Feature Services, Map Services, etc.) from the Portal.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
gis: Authenticated GIS connection.
|
|
165
|
+
query: Optional search query string to filter services.
|
|
166
|
+
owner: Optional owner username to filter services.
|
|
167
|
+
service_type: Optional service type filter (e.g., "Feature Service", "Map Service").
|
|
168
|
+
Defaults to empty string to list Feature Services.
|
|
169
|
+
max_results: Maximum number of services to return (default: 100).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of dictionaries containing service information (id, title, owner, type, url).
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
RuntimeError: If service search fails.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
# Build search query components
|
|
179
|
+
# Default to Feature Service if no type specified
|
|
180
|
+
if service_type:
|
|
181
|
+
query_parts = [f'type:"{service_type}"']
|
|
182
|
+
else:
|
|
183
|
+
query_parts = ['type:"Feature Service"']
|
|
184
|
+
|
|
185
|
+
if owner:
|
|
186
|
+
query_parts.append(f'owner:{owner}')
|
|
187
|
+
|
|
188
|
+
if query:
|
|
189
|
+
query_parts.append(query)
|
|
190
|
+
|
|
191
|
+
# Combine all query parts with AND
|
|
192
|
+
search_query = ' AND '.join(query_parts)
|
|
193
|
+
items = gis.content.search(query=search_query, max_items=max_results)
|
|
194
|
+
|
|
195
|
+
result = []
|
|
196
|
+
for item in items:
|
|
197
|
+
result.append({
|
|
198
|
+
"id": getattr(item, "id", ""),
|
|
199
|
+
"title": getattr(item, "title", ""),
|
|
200
|
+
"owner": getattr(item, "owner", ""),
|
|
201
|
+
"type": getattr(item, "type", ""),
|
|
202
|
+
"url": getattr(item, "url", ""),
|
|
203
|
+
})
|
|
204
|
+
return result
|
|
205
|
+
except Exception as search_error:
|
|
206
|
+
msg = f"Failed to search services: {search_error}"
|
|
207
|
+
raise RuntimeError(msg) from search_error
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---- Layer Operations ---------------------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_operational_layers(
|
|
214
|
+
map_data: dict[str, Any],
|
|
215
|
+
) -> list[dict[str, Any]]:
|
|
216
|
+
"""Extract operational layers from web map JSON.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
map_data: Web map JSON dictionary.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of operational layer dictionaries.
|
|
223
|
+
"""
|
|
224
|
+
return map_data.get("operationalLayers", [])
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_basemap_layers(
|
|
228
|
+
map_data: dict[str, Any],
|
|
229
|
+
) -> list[dict[str, Any]]:
|
|
230
|
+
"""Extract basemap layers from web map JSON.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
map_data: Web map JSON dictionary.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of basemap layer dictionaries.
|
|
237
|
+
"""
|
|
238
|
+
basemap = map_data.get("baseMap", {})
|
|
239
|
+
return basemap.get("baseMapLayers", [])
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_layer_by_id(
|
|
243
|
+
map_data: dict[str, Any],
|
|
244
|
+
layer_id: str,
|
|
245
|
+
) -> dict[str, Any] | None:
|
|
246
|
+
"""Find a layer by its ID.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
map_data: Web map JSON dictionary.
|
|
250
|
+
layer_id: Layer ID to find.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Layer dictionary or None if not found.
|
|
254
|
+
"""
|
|
255
|
+
for layer in get_operational_layers(map_data):
|
|
256
|
+
if layer.get("id") == layer_id:
|
|
257
|
+
return layer
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_layer_ids(
|
|
262
|
+
map_data: dict[str, Any],
|
|
263
|
+
) -> list[str]:
|
|
264
|
+
"""Get all operational layer IDs.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
map_data: Web map JSON dictionary.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of layer IDs.
|
|
271
|
+
"""
|
|
272
|
+
return [
|
|
273
|
+
layer.get("id", "")
|
|
274
|
+
for layer in get_operational_layers(map_data)
|
|
275
|
+
if layer.get("id")
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---- Map Comparison Functions -------------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def compare_layers(
|
|
283
|
+
layers1: list[dict[str, Any]],
|
|
284
|
+
layers2: list[dict[str, Any]],
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""Compare two lists of layers.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
layers1: First layer list (e.g., from index).
|
|
290
|
+
layers2: Second layer list (e.g., from commit).
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary with added, removed, and modified layer info.
|
|
294
|
+
"""
|
|
295
|
+
ids1 = {layer.get("id"): layer for layer in layers1 if layer.get("id")}
|
|
296
|
+
ids2 = {layer.get("id"): layer for layer in layers2 if layer.get("id")}
|
|
297
|
+
|
|
298
|
+
added = [ids1[id] for id in ids1 if id not in ids2]
|
|
299
|
+
removed = [ids2[id] for id in ids2 if id not in ids1]
|
|
300
|
+
modified = []
|
|
301
|
+
|
|
302
|
+
for layer_id in ids1:
|
|
303
|
+
if layer_id in ids2:
|
|
304
|
+
if ids1[layer_id] != ids2[layer_id]:
|
|
305
|
+
modified.append({
|
|
306
|
+
"id": layer_id,
|
|
307
|
+
"old": ids2[layer_id],
|
|
308
|
+
"new": ids1[layer_id],
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"added": added,
|
|
313
|
+
"removed": removed,
|
|
314
|
+
"modified": modified,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---- Serialization Functions --------------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def save_map_json(
|
|
322
|
+
map_data: dict[str, Any],
|
|
323
|
+
filepath: Path,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Save web map JSON to file.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
map_data: Web map JSON dictionary.
|
|
329
|
+
filepath: Path to save file.
|
|
330
|
+
"""
|
|
331
|
+
filepath.write_text(json.dumps(map_data, indent=2))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def load_map_json(
|
|
335
|
+
filepath: Path,
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""Load web map JSON from file.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
filepath: Path to JSON file.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Web map JSON dictionary.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
RuntimeError: If file cannot be loaded.
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
return json.loads(filepath.read_text())
|
|
350
|
+
except Exception as load_error:
|
|
351
|
+
msg = f"Failed to load map JSON from {filepath}: {load_error}"
|
|
352
|
+
raise RuntimeError(msg) from load_error
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---- Map Creation Functions ---------------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create_empty_webmap(
|
|
359
|
+
title: str = "New Map",
|
|
360
|
+
spatial_reference: int = 102100,
|
|
361
|
+
) -> dict[str, Any]:
|
|
362
|
+
"""Create an empty web map JSON structure.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
title: Map title.
|
|
366
|
+
spatial_reference: Spatial reference WKID.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Empty web map JSON dictionary.
|
|
370
|
+
"""
|
|
371
|
+
return {
|
|
372
|
+
"operationalLayers": [],
|
|
373
|
+
"baseMap": {
|
|
374
|
+
"baseMapLayers": [],
|
|
375
|
+
"title": "Basemap",
|
|
376
|
+
},
|
|
377
|
+
"spatialReference": {
|
|
378
|
+
"wkid": spatial_reference,
|
|
379
|
+
},
|
|
380
|
+
"version": "2.28",
|
|
381
|
+
"authoringApp": "GitMap",
|
|
382
|
+
"authoringAppVersion": "0.1.0",
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|