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 ADDED
@@ -0,0 +1,46 @@
1
+ # GitMap Core
2
+
3
+ Core library for GitMap - Version control for ArcGIS web maps.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ - **models.py** - Data classes for Commit, Branch, Remote, RepoConfig
14
+ - **repository.py** - Local `.gitmap` repository management
15
+ - **connection.py** - Portal/AGOL authentication
16
+ - **maps.py** - Web map JSON operations
17
+ - **diff.py** - JSON comparison and layer diffing
18
+ - **merge.py** - Layer-level merge logic
19
+ - **remote.py** - Push/pull operations to Portal
20
+ - **communication.py** - Portal/AGOL user notifications and group helpers
21
+ - **compat.py** - ArcGIS API version compatibility layer (2.2.x-2.4.x)
22
+ - **context.py** - SQLite-backed context/event store for IDE agents
23
+ - **visualize.py** - Mermaid, ASCII, and HTML visualization of context graphs
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from gitmap_core import Commit, Branch, RepoConfig
29
+ from gitmap_core.repository import Repository, init_repository
30
+
31
+ # Initialize a new repository
32
+ repo = init_repository("/path/to/project")
33
+
34
+ # Create a commit
35
+ commit = repo.create_commit(message="Initial commit", author="John")
36
+
37
+ # List branches
38
+ branches = repo.list_branches()
39
+ ```
40
+
41
+ ## Dependencies
42
+
43
+ - `arcgis>=2.2.0,<3.0.0` - ArcGIS API for Python
44
+ - `deepdiff>=6.0.0` - JSON comparison
45
+
46
+
@@ -0,0 +1,100 @@
1
+ """GitMap Core Library.
2
+
3
+ Provides version control functionality for ArcGIS web maps, including
4
+ local repository management, Portal authentication, and map operations.
5
+
6
+ Execution Context:
7
+ Library package - imported by CLI and other applications
8
+
9
+ Dependencies:
10
+ - arcgis: Portal/AGOL interaction
11
+ - deepdiff: JSON comparison
12
+
13
+ Metadata:
14
+ Version: 0.5.0
15
+ Author: GitMap Team
16
+ """
17
+ from __future__ import annotations
18
+
19
+ # Core data models - loaded eagerly for common use cases
20
+ from gitmap_core.context import Annotation
21
+ from gitmap_core.context import ContextStore
22
+ from gitmap_core.context import Edge
23
+ from gitmap_core.context import Event
24
+ from gitmap_core.models import Branch
25
+ from gitmap_core.models import Commit
26
+ from gitmap_core.models import Remote
27
+ from gitmap_core.models import RepoConfig
28
+ from gitmap_core.visualize import GraphData
29
+
30
+ __version__ = "0.5.0"
31
+
32
+ # Public API - core data models loaded eagerly
33
+ __all__ = [
34
+ # Core data models (eager load for common access patterns)
35
+ "Annotation",
36
+ "Branch",
37
+ "Commit",
38
+ "ContextStore",
39
+ "Edge",
40
+ "Event",
41
+ "GraphData",
42
+ "Remote",
43
+ "RepoConfig",
44
+ "__version__",
45
+ # Visualization functions (lazy loaded - optional utilities)
46
+ "generate_ascii_graph",
47
+ "generate_ascii_timeline",
48
+ "generate_html_visualization",
49
+ "generate_mermaid_flowchart",
50
+ "generate_mermaid_git_graph",
51
+ "generate_mermaid_timeline",
52
+ "visualize_context",
53
+ ]
54
+
55
+
56
+ # ---- Lazy Import Functions ----------------------------------------------------------------------------------
57
+ # These are loaded on-demand to reduce initial import time and memory footprint.
58
+ # Only imported when actually called, avoiding unnecessary dependencies.
59
+
60
+
61
+ def generate_ascii_graph(*args, **kwargs):
62
+ """Generate ASCII representation of commit graph (lazy import)."""
63
+ from gitmap_core.visualize import generate_ascii_graph as _func
64
+ return _func(*args, **kwargs)
65
+
66
+
67
+ def generate_ascii_timeline(*args, **kwargs):
68
+ """Generate ASCII timeline visualization (lazy import)."""
69
+ from gitmap_core.visualize import generate_ascii_timeline as _func
70
+ return _func(*args, **kwargs)
71
+
72
+
73
+ def generate_html_visualization(*args, **kwargs):
74
+ """Generate HTML visualization (lazy import)."""
75
+ from gitmap_core.visualize import generate_html_visualization as _func
76
+ return _func(*args, **kwargs)
77
+
78
+
79
+ def generate_mermaid_flowchart(*args, **kwargs):
80
+ """Generate Mermaid flowchart (lazy import)."""
81
+ from gitmap_core.visualize import generate_mermaid_flowchart as _func
82
+ return _func(*args, **kwargs)
83
+
84
+
85
+ def generate_mermaid_git_graph(*args, **kwargs):
86
+ """Generate Mermaid git graph (lazy import)."""
87
+ from gitmap_core.visualize import generate_mermaid_git_graph as _func
88
+ return _func(*args, **kwargs)
89
+
90
+
91
+ def generate_mermaid_timeline(*args, **kwargs):
92
+ """Generate Mermaid timeline (lazy import)."""
93
+ from gitmap_core.visualize import generate_mermaid_timeline as _func
94
+ return _func(*args, **kwargs)
95
+
96
+
97
+ def visualize_context(*args, **kwargs):
98
+ """Visualize context graph (lazy import)."""
99
+ from gitmap_core.visualize import visualize_context as _func
100
+ return _func(*args, **kwargs)
@@ -0,0 +1,346 @@
1
+ """Communication helpers for Portal/AGOL users and groups.
2
+
3
+ Provides utilities to gather group members and send notifications using the
4
+ ArcGIS API for Python `Group.notify` method (no SMTP wiring required).
5
+
6
+ Execution Context:
7
+ Library module - imported by CLI notification commands
8
+
9
+ Dependencies:
10
+ - arcgis: Portal/AGOL interaction
11
+
12
+ Metadata:
13
+ Version: 0.2.0
14
+ Author: GitMap Team
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import Sequence
19
+
20
+ try:
21
+ from arcgis.gis import GIS # type: ignore
22
+ except Exception: # pragma: no cover - handled at runtime
23
+ GIS = None # type: ignore
24
+
25
+
26
+ def _ensure_gis(gis: GIS) -> None:
27
+ """Ensure a GIS connection object is available.
28
+
29
+ Args:
30
+ gis: GIS connection object.
31
+
32
+ Raises:
33
+ RuntimeError: If GIS is not available.
34
+ """
35
+ if GIS is None:
36
+ msg = "ArcGIS API for Python is not installed."
37
+ raise RuntimeError(msg)
38
+ if gis is None:
39
+ msg = "A valid GIS connection is required."
40
+ raise RuntimeError(msg)
41
+
42
+
43
+ def _resolve_group(gis: GIS, group_id_or_title: str):
44
+ """Resolve a group by ID or title.
45
+
46
+ Args:
47
+ gis: Authenticated GIS connection.
48
+ group_id_or_title: Group ID or title to resolve.
49
+
50
+ Returns:
51
+ Group object if found, otherwise None.
52
+ """
53
+ _ensure_gis(gis)
54
+ group = gis.groups.get(group_id_or_title)
55
+ if group:
56
+ return group
57
+
58
+ matches = gis.groups.search(f'title:"{group_id_or_title}"')
59
+ return matches[0] if matches else None
60
+
61
+
62
+ def get_group_member_usernames(
63
+ gis: GIS,
64
+ group_id_or_title: str,
65
+ ) -> list[str]:
66
+ """Collect usernames for all members of a group.
67
+
68
+ Args:
69
+ gis: Authenticated GIS connection.
70
+ group_id_or_title: Group ID or title.
71
+
72
+ Returns:
73
+ List of unique usernames (owner, admins, users, and invited users).
74
+
75
+ Raises:
76
+ RuntimeError: If group cannot be found or no members are available.
77
+ """
78
+ _ensure_gis(gis)
79
+ group = _resolve_group(gis, group_id_or_title)
80
+ if group is None:
81
+ msg = f"Group '{group_id_or_title}' not found."
82
+ raise RuntimeError(msg)
83
+
84
+ members = group.get_members()
85
+ usernames: set[str] = set()
86
+
87
+ owner = members.get("owner")
88
+ if owner:
89
+ usernames.add(owner)
90
+
91
+ for key in ("admins", "users", "admins_invited", "users_invited"):
92
+ for username in members.get(key, []) or []:
93
+ if username:
94
+ usernames.add(username)
95
+
96
+ if not usernames:
97
+ msg = f"No members found for group '{group_id_or_title}'."
98
+ raise RuntimeError(msg)
99
+
100
+ return sorted(usernames)
101
+
102
+
103
+ def list_groups(
104
+ gis: GIS,
105
+ query: str = "",
106
+ max_results: int = 100,
107
+ ) -> list[dict[str, str]]:
108
+ """List available groups from the Portal.
109
+
110
+ Args:
111
+ gis: Authenticated GIS connection.
112
+ query: Optional search query string to filter groups (e.g., "title:MyGroup").
113
+ Defaults to empty string to list all groups.
114
+ max_results: Maximum number of groups to return (default: 100).
115
+
116
+ Returns:
117
+ List of dictionaries containing group information (id, title, owner).
118
+
119
+ Raises:
120
+ RuntimeError: If group search fails.
121
+ """
122
+ _ensure_gis(gis)
123
+ try:
124
+ # groups.search() only accepts query string as positional argument
125
+ groups = gis.groups.search(query)
126
+ result = []
127
+ for group in groups[:max_results]:
128
+ result.append({
129
+ "id": getattr(group, "id", ""),
130
+ "title": getattr(group, "title", ""),
131
+ "owner": getattr(group, "owner", ""),
132
+ })
133
+ return result
134
+ except Exception as search_error:
135
+ msg = f"Failed to search groups: {search_error}"
136
+ raise RuntimeError(msg) from search_error
137
+
138
+
139
+ def send_group_notification(
140
+ gis: GIS,
141
+ group_id_or_title: str,
142
+ subject: str,
143
+ body: str,
144
+ users: Sequence[str] | None = None,
145
+ ) -> Sequence[str]:
146
+ """Send a notification to group members using ArcGIS `Group.notify`.
147
+
148
+ Args:
149
+ gis: Authenticated GIS connection.
150
+ group_id_or_title: Group ID or title to resolve.
151
+ subject: Notification subject.
152
+ body: Notification message body (plain text or HTML supported by ArcGIS).
153
+ users: Optional iterable of usernames; when omitted, the entire group is notified.
154
+
155
+ Returns:
156
+ Sequence of usernames passed to the notification call.
157
+
158
+ Raises:
159
+ RuntimeError: If notification fails or no users can be determined.
160
+ """
161
+ _ensure_gis(gis)
162
+ group = _resolve_group(gis, group_id_or_title)
163
+ if group is None:
164
+ msg = f"Group '{group_id_or_title}' not found."
165
+ raise RuntimeError(msg)
166
+
167
+ target_users = list(users) if users else get_group_member_usernames(gis, group_id_or_title)
168
+ if not target_users:
169
+ msg = f"No target users resolved for group '{group_id_or_title}'."
170
+ raise RuntimeError(msg)
171
+
172
+ try:
173
+ group.notify(users=target_users, subject=subject, message=body)
174
+ except Exception as notify_error: # pragma: no cover - depends on ArcGIS service
175
+ msg = f"Failed to send notification: {notify_error}"
176
+ raise RuntimeError(msg) from notify_error
177
+
178
+ return target_users
179
+
180
+
181
+ def get_item_group_users(
182
+ gis: GIS,
183
+ item,
184
+ ) -> list[str]:
185
+ """Collect all users from groups that have access to a Portal item.
186
+
187
+ Gets all groups that the item is shared with and collects unique usernames
188
+ from all those groups. De-duplicates users across groups.
189
+
190
+ Args:
191
+ gis: Authenticated GIS connection.
192
+ item: Portal Item object.
193
+
194
+ Returns:
195
+ List of unique usernames from all groups sharing the item.
196
+
197
+ Raises:
198
+ RuntimeError: If item sharing information cannot be retrieved.
199
+ """
200
+ _ensure_gis(gis)
201
+ try:
202
+ # Get item sharing information
203
+ # item.sharing is a SharingManager object, not a dict
204
+ # Check if item is private (not shared)
205
+ if item.access == "private":
206
+ return []
207
+
208
+ # Get groups the item is shared with
209
+ # Try to get from item properties or by querying user's groups
210
+ groups = []
211
+ try:
212
+ # Method 1: Try to get from item's properties
213
+ if hasattr(item, "properties") and item.properties:
214
+ sharing_data = item.properties.get("sharing", {})
215
+ if isinstance(sharing_data, dict):
216
+ groups = sharing_data.get("groups", [])
217
+
218
+ # Method 2: If not found, query user's groups to find which have access
219
+ if not groups:
220
+ user = gis.users.me
221
+ if user:
222
+ user_groups = user.groups
223
+ for group in user_groups:
224
+ try:
225
+ # Check if this group has access to the item
226
+ # by checking if the item appears in the group's content
227
+ group_items = group.content()
228
+ if any(g_item.id == item.id for g_item in group_items):
229
+ groups.append(group.id)
230
+ except Exception:
231
+ continue
232
+ except Exception:
233
+ groups = []
234
+
235
+ if not groups:
236
+ return []
237
+
238
+ # Collect all unique usernames from all groups
239
+ all_users: set[str] = set()
240
+ for group_id in groups:
241
+ try:
242
+ group_users = get_group_member_usernames(gis, group_id)
243
+ all_users.update(group_users)
244
+ except Exception:
245
+ # Skip groups that can't be accessed
246
+ continue
247
+
248
+ return sorted(all_users)
249
+
250
+ except Exception as sharing_error:
251
+ msg = f"Failed to get item group users: {sharing_error}"
252
+ raise RuntimeError(msg) from sharing_error
253
+
254
+
255
+ def notify_item_group_users(
256
+ gis: GIS,
257
+ item,
258
+ subject: str,
259
+ body: str,
260
+ ) -> list[str]:
261
+ """Send notifications to all users from groups that have access to an item.
262
+
263
+ Gets all groups that the item is shared with, collects unique usernames
264
+ from all those groups, and sends notifications. The ArcGIS API handles
265
+ deduplication of notifications.
266
+
267
+ Args:
268
+ gis: Authenticated GIS connection.
269
+ item: Portal Item object.
270
+ subject: Notification subject.
271
+ body: Notification message body.
272
+
273
+ Returns:
274
+ List of unique usernames from all groups (may receive notifications
275
+ through multiple groups, but ArcGIS handles deduplication).
276
+
277
+ Raises:
278
+ RuntimeError: If notification fails.
279
+ """
280
+ _ensure_gis(gis)
281
+ try:
282
+ # Get item sharing information
283
+ # item.sharing is a SharingManager object, not a dict
284
+ # Check if item is private (not shared)
285
+ if item.access == "private":
286
+ return []
287
+
288
+ # Get groups the item is shared with
289
+ # Try to get from item properties or by querying user's groups
290
+ groups = []
291
+ try:
292
+ # Method 1: Try to get from item's properties
293
+ if hasattr(item, "properties") and item.properties:
294
+ sharing_data = item.properties.get("sharing", {})
295
+ if isinstance(sharing_data, dict):
296
+ groups = sharing_data.get("groups", [])
297
+
298
+ # Method 2: If not found, query user's groups to find which have access
299
+ if not groups:
300
+ user = gis.users.me
301
+ if user:
302
+ user_groups = user.groups
303
+ for group in user_groups:
304
+ try:
305
+ # Check if this group has access to the item
306
+ # by checking if the item appears in the group's content
307
+ group_items = group.content()
308
+ if any(g_item.id == item.id for g_item in group_items):
309
+ groups.append(group.id)
310
+ except Exception:
311
+ continue
312
+ except Exception:
313
+ groups = []
314
+
315
+ if not groups:
316
+ return []
317
+
318
+ # Collect all unique usernames from all groups
319
+ all_users: set[str] = set()
320
+
321
+ for group_id in groups:
322
+ try:
323
+ group_users = get_group_member_usernames(gis, group_id)
324
+ all_users.update(group_users)
325
+
326
+ # Send notification to each group
327
+ # ArcGIS API handles deduplication of notifications
328
+ try:
329
+ group = _resolve_group(gis, group_id)
330
+ if group:
331
+ group.notify(users=group_users, subject=subject, message=body)
332
+ except Exception:
333
+ # Skip groups that fail to notify, but keep their users in the list
334
+ continue
335
+
336
+ except Exception:
337
+ # Skip groups that can't be accessed
338
+ continue
339
+
340
+ return sorted(all_users)
341
+
342
+ except Exception as notify_error:
343
+ msg = f"Failed to notify item group users: {notify_error}"
344
+ raise RuntimeError(msg) from notify_error
345
+
346
+