google-analytics-cli 0.1.0rc1__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.
Files changed (49) hide show
  1. ga_cli/__init__.py +8 -0
  2. ga_cli/api/__init__.py +0 -0
  3. ga_cli/api/client.py +142 -0
  4. ga_cli/auth/__init__.py +35 -0
  5. ga_cli/auth/credentials.py +126 -0
  6. ga_cli/auth/oauth.py +322 -0
  7. ga_cli/auth/service_account.py +155 -0
  8. ga_cli/commands/__init__.py +0 -0
  9. ga_cli/commands/access_bindings.py +254 -0
  10. ga_cli/commands/access_reports.py +201 -0
  11. ga_cli/commands/account_summaries.py +68 -0
  12. ga_cli/commands/accounts.py +297 -0
  13. ga_cli/commands/agent_cmd.py +776 -0
  14. ga_cli/commands/annotations.py +264 -0
  15. ga_cli/commands/audiences.py +223 -0
  16. ga_cli/commands/auth_cmd.py +205 -0
  17. ga_cli/commands/bigquery_links.py +309 -0
  18. ga_cli/commands/calculated_metrics.py +312 -0
  19. ga_cli/commands/channel_groups.py +223 -0
  20. ga_cli/commands/completions_cmd.py +55 -0
  21. ga_cli/commands/config_cmd.py +113 -0
  22. ga_cli/commands/custom_dimensions.py +272 -0
  23. ga_cli/commands/custom_metrics.py +305 -0
  24. ga_cli/commands/data_retention.py +153 -0
  25. ga_cli/commands/data_streams.py +277 -0
  26. ga_cli/commands/event_create_rules.py +250 -0
  27. ga_cli/commands/event_edit_rules.py +292 -0
  28. ga_cli/commands/firebase_links.py +142 -0
  29. ga_cli/commands/google_ads_links.py +225 -0
  30. ga_cli/commands/key_events.py +269 -0
  31. ga_cli/commands/mp_secrets.py +265 -0
  32. ga_cli/commands/properties.py +330 -0
  33. ga_cli/commands/property_settings.py +287 -0
  34. ga_cli/commands/reports.py +726 -0
  35. ga_cli/commands/upgrade_cmd.py +148 -0
  36. ga_cli/config/__init__.py +0 -0
  37. ga_cli/config/constants.py +61 -0
  38. ga_cli/config/store.py +115 -0
  39. ga_cli/main.py +110 -0
  40. ga_cli/utils/__init__.py +20 -0
  41. ga_cli/utils/describe.py +129 -0
  42. ga_cli/utils/dry_run.py +40 -0
  43. ga_cli/utils/errors.py +150 -0
  44. ga_cli/utils/output.py +209 -0
  45. ga_cli/utils/pagination.py +93 -0
  46. google_analytics_cli-0.1.0rc1.dist-info/METADATA +269 -0
  47. google_analytics_cli-0.1.0rc1.dist-info/RECORD +49 -0
  48. google_analytics_cli-0.1.0rc1.dist-info/WHEEL +4 -0
  49. google_analytics_cli-0.1.0rc1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,297 @@
1
+ """Account management commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import questionary
6
+ import typer
7
+
8
+ from ..api.client import get_admin_client
9
+ from ..config.store import get_effective_value
10
+ from ..utils import (
11
+ handle_dry_run,
12
+ handle_error,
13
+ info,
14
+ output,
15
+ require_options,
16
+ resolve_output_format,
17
+ success,
18
+ )
19
+ from ..utils.pagination import paginate_all
20
+
21
+ accounts_app = typer.Typer(name="accounts", help="Manage GA4 accounts", no_args_is_help=True)
22
+
23
+
24
+ @accounts_app.command("list")
25
+ def list_cmd(
26
+ output_format: Optional[str] = typer.Option(
27
+ None, "--output", "-o", help="Output format (json, table, compact)"
28
+ ),
29
+ ):
30
+ """List all accessible GA4 accounts."""
31
+ try:
32
+ effective_format = resolve_output_format(output_format)
33
+
34
+ admin = get_admin_client()
35
+ accounts = paginate_all(
36
+ lambda **kw: admin.accounts().list(**kw).execute(),
37
+ "accounts",
38
+ pageSize=200,
39
+ )
40
+ output(
41
+ accounts,
42
+ effective_format,
43
+ columns=["name", "displayName", "createTime"],
44
+ headers=["Resource Name", "Display Name", "Created"],
45
+ )
46
+ except Exception as e:
47
+ handle_error(e)
48
+
49
+
50
+ @accounts_app.command("get")
51
+ def get_cmd(
52
+ account_id: Optional[str] = typer.Option(
53
+ None, "--account-id", "-a", help="Account ID (numeric)"
54
+ ),
55
+ output_format: Optional[str] = typer.Option(
56
+ None, "--output", "-o", help="Output format (json, table, compact)"
57
+ ),
58
+ ):
59
+ """Get details for a specific account."""
60
+ try:
61
+ effective_account = get_effective_value(account_id, "default_account_id")
62
+ require_options({"account_id": effective_account}, ["account_id"])
63
+ effective_format = resolve_output_format(output_format)
64
+
65
+ admin = get_admin_client()
66
+ account = admin.accounts().get(name=f"accounts/{effective_account}").execute()
67
+ output(account, effective_format)
68
+ except Exception as e:
69
+ handle_error(e)
70
+
71
+
72
+ @accounts_app.command("update")
73
+ def update_cmd(
74
+ account_id: Optional[str] = typer.Option(
75
+ None, "--account-id", "-a", help="Account ID (numeric)"
76
+ ),
77
+ name: str = typer.Option(..., "--name", help="New display name"),
78
+ dry_run: bool = typer.Option(
79
+ False, "--dry-run", help="Preview the request without executing"
80
+ ),
81
+ output_format: Optional[str] = typer.Option(
82
+ None, "--output", "-o", help="Output format (json, table, compact)"
83
+ ),
84
+ ):
85
+ """Update a GA4 account."""
86
+ try:
87
+ effective_account = get_effective_value(account_id, "default_account_id")
88
+ require_options({"account_id": effective_account}, ["account_id"])
89
+ effective_format = resolve_output_format(output_format)
90
+
91
+ body = {"displayName": name}
92
+ if dry_run:
93
+ handle_dry_run(
94
+ "update", "PATCH", f"accounts/{effective_account}",
95
+ body, update_mask="displayName",
96
+ )
97
+
98
+ admin = get_admin_client()
99
+ account = (
100
+ admin.accounts()
101
+ .patch(
102
+ name=f"accounts/{effective_account}",
103
+ body=body,
104
+ updateMask="displayName",
105
+ )
106
+ .execute()
107
+ )
108
+ output(account, effective_format)
109
+ except typer.Exit:
110
+ raise
111
+ except Exception as e:
112
+ handle_error(e)
113
+
114
+
115
+ @accounts_app.command("delete")
116
+ def delete_cmd(
117
+ account_id: Optional[str] = typer.Option(
118
+ None, "--account-id", "-a", help="Account ID (numeric)"
119
+ ),
120
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
121
+ dry_run: bool = typer.Option(
122
+ False, "--dry-run", help="Preview the request without executing"
123
+ ),
124
+ ):
125
+ """Delete a GA4 account (soft delete, moves to trash)."""
126
+ try:
127
+ effective_account = get_effective_value(account_id, "default_account_id")
128
+ require_options({"account_id": effective_account}, ["account_id"])
129
+
130
+ if dry_run:
131
+ handle_dry_run("delete", "DELETE", f"accounts/{effective_account}", None)
132
+
133
+ if not yes:
134
+ confirmed = questionary.confirm(
135
+ f"Delete account {effective_account}? "
136
+ "All child resources (properties, streams, links) will be trashed."
137
+ ).ask()
138
+ if not confirmed:
139
+ info("Cancelled.")
140
+ raise typer.Exit()
141
+
142
+ admin = get_admin_client()
143
+ admin.accounts().delete(name=f"accounts/{effective_account}").execute()
144
+ success(f"Account {effective_account} deleted (moved to trash).")
145
+ except typer.Exit:
146
+ raise
147
+ except Exception as e:
148
+ handle_error(e)
149
+
150
+
151
+ @accounts_app.command("get-data-sharing")
152
+ def get_data_sharing_cmd(
153
+ account_id: Optional[str] = typer.Option(
154
+ None, "--account-id", "-a", help="Account ID (numeric)"
155
+ ),
156
+ output_format: Optional[str] = typer.Option(
157
+ None, "--output", "-o", help="Output format (json, table, compact)"
158
+ ),
159
+ ):
160
+ """Get data sharing settings for an account (read-only)."""
161
+ try:
162
+ effective_account = get_effective_value(account_id, "default_account_id")
163
+ require_options({"account_id": effective_account}, ["account_id"])
164
+ effective_format = resolve_output_format(output_format)
165
+
166
+ admin = get_admin_client()
167
+ settings = (
168
+ admin.accounts()
169
+ .getDataSharingSettings(name=f"accounts/{effective_account}/dataSharingSettings")
170
+ .execute()
171
+ )
172
+ output(
173
+ settings,
174
+ effective_format,
175
+ columns=[
176
+ "name",
177
+ "sharingWithGoogleSupportEnabled",
178
+ "sharingWithGoogleAssignedSalesEnabled",
179
+ "sharingWithGoogleProductsEnabled",
180
+ "sharingWithOthersEnabled",
181
+ ],
182
+ headers=[
183
+ "Resource Name",
184
+ "Google Support",
185
+ "Google Sales",
186
+ "Google Products",
187
+ "Others",
188
+ ],
189
+ )
190
+ except Exception as e:
191
+ handle_error(e)
192
+
193
+
194
+ def _extract_resource_name(change: dict) -> str:
195
+ """Extract a human-readable resource name from a change entry."""
196
+ for key in ("resourceAfterChange", "resourceBeforeChange"):
197
+ container = change.get(key)
198
+ if not container:
199
+ continue
200
+ for resource_obj in container.values():
201
+ if isinstance(resource_obj, dict):
202
+ return resource_obj.get("displayName") or resource_obj.get("name", "")
203
+ return ""
204
+
205
+
206
+ def _flatten_change_events(events: list) -> list[dict]:
207
+ """Flatten change history events into one row per change."""
208
+ rows = []
209
+ for event in events:
210
+ for change in event.get("changes", []):
211
+ rows.append(
212
+ {
213
+ "changeTime": event.get("changeTime", ""),
214
+ "actor": event.get("userActorEmail") or event.get("actorType", ""),
215
+ "resourceType": change.get("resource", ""),
216
+ "action": change.get("action", ""),
217
+ "resourceName": _extract_resource_name(change),
218
+ }
219
+ )
220
+ return rows
221
+
222
+
223
+ @accounts_app.command("change-history")
224
+ def change_history_cmd(
225
+ account_id: Optional[str] = typer.Option(
226
+ None, "--account-id", "-a", help="Account ID (numeric)"
227
+ ),
228
+ property_id: Optional[str] = typer.Option(
229
+ None, "--property-id", "-p", help="Filter to specific property"
230
+ ),
231
+ resource_type: Optional[str] = typer.Option(
232
+ None, "--resource-type", help="Filter by resource type (ACCOUNT, PROPERTY, etc.)"
233
+ ),
234
+ action: Optional[str] = typer.Option(
235
+ None, "--action", help="Filter by action (CREATED, UPDATED, DELETED)"
236
+ ),
237
+ earliest_change_time: Optional[str] = typer.Option(
238
+ None, "--since", help="Earliest change time (ISO 8601)"
239
+ ),
240
+ latest_change_time: Optional[str] = typer.Option(
241
+ None, "--until", help="Latest change time (ISO 8601)"
242
+ ),
243
+ limit: int = typer.Option(100, "--limit", "-l", help="Max results to return"),
244
+ output_format: Optional[str] = typer.Option(
245
+ None, "--output", "-o", help="Output format (json, table, compact)"
246
+ ),
247
+ ) -> None:
248
+ """Search change history events for an account."""
249
+ try:
250
+ effective_account = get_effective_value(account_id, "default_account_id")
251
+ require_options({"account_id": effective_account}, ["account_id"])
252
+ effective_format = resolve_output_format(output_format)
253
+
254
+ body: dict = {}
255
+ if property_id:
256
+ body["property"] = f"properties/{property_id}"
257
+ if resource_type:
258
+ body["resourceType"] = [resource_type.upper()]
259
+ if action:
260
+ body["action"] = [action.upper()]
261
+ if earliest_change_time:
262
+ body["earliestChangeTime"] = earliest_change_time
263
+ if latest_change_time:
264
+ body["latestChangeTime"] = latest_change_time
265
+
266
+ admin = get_admin_client()
267
+ events = paginate_all(
268
+ lambda **kw: (
269
+ admin.accounts()
270
+ .searchChangeHistoryEvents(
271
+ account=f"accounts/{effective_account}",
272
+ body={**body, **kw},
273
+ )
274
+ .execute()
275
+ ),
276
+ "changeHistoryEvents",
277
+ pageSize=200,
278
+ )
279
+
280
+ events = events[:limit]
281
+
282
+ if not events:
283
+ info("No changes found.")
284
+ return
285
+
286
+ if effective_format == "json":
287
+ output(events, effective_format)
288
+ else:
289
+ rows = _flatten_change_events(events)
290
+ output(
291
+ rows,
292
+ effective_format,
293
+ columns=["changeTime", "actor", "resourceType", "action", "resourceName"],
294
+ headers=["Time", "Actor", "Resource Type", "Action", "Resource Name"],
295
+ )
296
+ except Exception as e:
297
+ handle_error(e)