google-analytics-cli 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.
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.0.dist-info/METADATA +321 -0
  47. google_analytics_cli-0.1.0.dist-info/RECORD +49 -0
  48. google_analytics_cli-0.1.0.dist-info/WHEEL +4 -0
  49. google_analytics_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,277 @@
1
+ """Data stream 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
+ data_streams_app = typer.Typer(
22
+ name="data-streams", help="Manage GA4 data streams", no_args_is_help=True
23
+ )
24
+
25
+ # Stream types that require additional configuration
26
+ _WEB_STREAM = "WEB_DATA_STREAM"
27
+ _ANDROID_STREAM = "ANDROID_APP_DATA_STREAM"
28
+ _IOS_STREAM = "IOS_APP_DATA_STREAM"
29
+
30
+
31
+ @data_streams_app.command("list")
32
+ def list_cmd(
33
+ property_id: Optional[str] = typer.Option(
34
+ None, "--property-id", "-p", help="Property ID (numeric)"
35
+ ),
36
+ output_format: Optional[str] = typer.Option(
37
+ None, "--output", "-o", help="Output format (json, table, compact)"
38
+ ),
39
+ ):
40
+ """List data streams for a property."""
41
+ try:
42
+ effective_property = get_effective_value(property_id, "default_property_id")
43
+ require_options({"property_id": effective_property}, ["property_id"])
44
+ effective_format = resolve_output_format(output_format)
45
+
46
+ admin = get_admin_client()
47
+ streams = paginate_all(
48
+ lambda **kw: admin.properties()
49
+ .dataStreams()
50
+ .list(parent=f"properties/{effective_property}", **kw)
51
+ .execute(),
52
+ "dataStreams",
53
+ pageSize=200,
54
+ )
55
+
56
+ output(
57
+ streams,
58
+ effective_format,
59
+ columns=["name", "type", "displayName", "createTime"],
60
+ headers=["Resource Name", "Type", "Display Name", "Created"],
61
+ )
62
+ except Exception as e:
63
+ handle_error(e)
64
+
65
+
66
+ @data_streams_app.command("get")
67
+ def get_cmd(
68
+ property_id: Optional[str] = typer.Option(
69
+ None, "--property-id", "-p", help="Property ID (numeric)"
70
+ ),
71
+ stream_id: str = typer.Option(
72
+ ..., "--stream-id", "-s", help="Data Stream ID"
73
+ ),
74
+ output_format: Optional[str] = typer.Option(
75
+ None, "--output", "-o", help="Output format (json, table, compact)"
76
+ ),
77
+ ):
78
+ """Get data stream details."""
79
+ try:
80
+ effective_property = get_effective_value(property_id, "default_property_id")
81
+ require_options({"property_id": effective_property}, ["property_id"])
82
+ effective_format = resolve_output_format(output_format)
83
+
84
+ admin = get_admin_client()
85
+ stream = (
86
+ admin.properties()
87
+ .dataStreams()
88
+ .get(
89
+ name=f"properties/{effective_property}/dataStreams/{stream_id}",
90
+ )
91
+ .execute()
92
+ )
93
+ output(stream, effective_format)
94
+ except Exception as e:
95
+ handle_error(e)
96
+
97
+
98
+ @data_streams_app.command("create")
99
+ def create_cmd(
100
+ property_id: Optional[str] = typer.Option(
101
+ None, "--property-id", "-p", help="Property ID (numeric)"
102
+ ),
103
+ display_name: str = typer.Option(
104
+ ..., "--display-name", help="Stream display name"
105
+ ),
106
+ stream_type: str = typer.Option(
107
+ _WEB_STREAM,
108
+ "--type",
109
+ "-t",
110
+ help="Stream type: WEB_DATA_STREAM, ANDROID_APP_DATA_STREAM, IOS_APP_DATA_STREAM",
111
+ ),
112
+ url: Optional[str] = typer.Option(
113
+ None, "--url", help="Default URI (required for web streams)"
114
+ ),
115
+ bundle_id: Optional[str] = typer.Option(
116
+ None, "--bundle-id", help="App bundle ID (required for app streams)"
117
+ ),
118
+ dry_run: bool = typer.Option(
119
+ False, "--dry-run", help="Preview the request without executing"
120
+ ),
121
+ output_format: Optional[str] = typer.Option(
122
+ None, "--output", "-o", help="Output format (json, table, compact)"
123
+ ),
124
+ ):
125
+ """Create a new data stream."""
126
+ try:
127
+ effective_property = get_effective_value(property_id, "default_property_id")
128
+ require_options({"property_id": effective_property}, ["property_id"])
129
+ effective_format = resolve_output_format(output_format)
130
+
131
+ body = {
132
+ "displayName": display_name,
133
+ "type": stream_type,
134
+ }
135
+
136
+ # Validate and add type-specific data
137
+ if stream_type == _WEB_STREAM:
138
+ if not url:
139
+ raise typer.BadParameter(
140
+ "--url is required for WEB_DATA_STREAM type"
141
+ )
142
+ body["webStreamData"] = {"defaultUri": url}
143
+ elif stream_type == _ANDROID_STREAM:
144
+ if not bundle_id:
145
+ raise typer.BadParameter(
146
+ "--bundle-id is required for ANDROID_APP_DATA_STREAM type"
147
+ )
148
+ body["androidAppStreamData"] = {"packageName": bundle_id}
149
+ elif stream_type == _IOS_STREAM:
150
+ if not bundle_id:
151
+ raise typer.BadParameter(
152
+ "--bundle-id is required for IOS_APP_DATA_STREAM type"
153
+ )
154
+ body["iosAppStreamData"] = {"bundleId": bundle_id}
155
+
156
+ if dry_run:
157
+ handle_dry_run("create", "POST", f"properties/{effective_property}", body)
158
+
159
+ admin = get_admin_client()
160
+ stream = (
161
+ admin.properties()
162
+ .dataStreams()
163
+ .create(parent=f"properties/{effective_property}", body=body)
164
+ .execute()
165
+ )
166
+ output(stream, effective_format)
167
+ except (typer.BadParameter, typer.Exit):
168
+ raise
169
+ except Exception as e:
170
+ handle_error(e)
171
+
172
+
173
+ @data_streams_app.command("update")
174
+ def update_cmd(
175
+ property_id: Optional[str] = typer.Option(
176
+ None, "--property-id", "-p", help="Property ID (numeric)"
177
+ ),
178
+ stream_id: str = typer.Option(
179
+ ..., "--stream-id", "-s", help="Data Stream ID"
180
+ ),
181
+ display_name: Optional[str] = typer.Option(
182
+ None, "--display-name", help="New display name"
183
+ ),
184
+ dry_run: bool = typer.Option(
185
+ False, "--dry-run", help="Preview the request without executing"
186
+ ),
187
+ output_format: Optional[str] = typer.Option(
188
+ None, "--output", "-o", help="Output format (json, table, compact)"
189
+ ),
190
+ ):
191
+ """Update a data stream."""
192
+ try:
193
+ effective_property = get_effective_value(property_id, "default_property_id")
194
+ require_options({"property_id": effective_property}, ["property_id"])
195
+ effective_format = resolve_output_format(output_format)
196
+
197
+ field_map = {
198
+ "displayName": display_name,
199
+ }
200
+ body = {k: v for k, v in field_map.items() if v is not None}
201
+
202
+ if not body:
203
+ raise typer.BadParameter(
204
+ "At least one of --display-name must be specified."
205
+ )
206
+
207
+ update_mask = ",".join(body.keys())
208
+
209
+ if dry_run:
210
+ handle_dry_run(
211
+ "update", "PATCH",
212
+ f"properties/{effective_property}/dataStreams/{stream_id}",
213
+ body, update_mask=update_mask,
214
+ )
215
+
216
+ admin = get_admin_client()
217
+ stream = (
218
+ admin.properties()
219
+ .dataStreams()
220
+ .patch(
221
+ name=f"properties/{effective_property}/dataStreams/{stream_id}",
222
+ body=body,
223
+ updateMask=update_mask,
224
+ )
225
+ .execute()
226
+ )
227
+ output(stream, effective_format)
228
+ except (typer.BadParameter, typer.Exit):
229
+ raise
230
+ except Exception as e:
231
+ handle_error(e)
232
+
233
+
234
+ @data_streams_app.command("delete")
235
+ def delete_cmd(
236
+ property_id: Optional[str] = typer.Option(
237
+ None, "--property-id", "-p", help="Property ID (numeric)"
238
+ ),
239
+ stream_id: str = typer.Option(
240
+ ..., "--stream-id", "-s", help="Data Stream ID"
241
+ ),
242
+ yes: bool = typer.Option(
243
+ False, "--yes", "-y", help="Skip confirmation prompt"
244
+ ),
245
+ dry_run: bool = typer.Option(
246
+ False, "--dry-run", help="Preview the request without executing"
247
+ ),
248
+ ):
249
+ """Delete a data stream."""
250
+ try:
251
+ effective_property = get_effective_value(property_id, "default_property_id")
252
+ require_options({"property_id": effective_property}, ["property_id"])
253
+
254
+ if dry_run:
255
+ handle_dry_run(
256
+ "delete", "DELETE",
257
+ f"properties/{effective_property}/dataStreams/{stream_id}",
258
+ None,
259
+ )
260
+
261
+ if not yes:
262
+ confirmed = questionary.confirm(
263
+ f"Delete data stream {stream_id}? This cannot be undone."
264
+ ).ask()
265
+ if not confirmed:
266
+ info("Cancelled.")
267
+ raise typer.Exit()
268
+
269
+ admin = get_admin_client()
270
+ admin.properties().dataStreams().delete(
271
+ name=f"properties/{effective_property}/dataStreams/{stream_id}",
272
+ ).execute()
273
+ success(f"Data stream {stream_id} deleted.")
274
+ except typer.Exit:
275
+ raise
276
+ except Exception as e:
277
+ handle_error(e)
@@ -0,0 +1,250 @@
1
+ """Event create rule management commands."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import questionary
8
+ import typer
9
+
10
+ from ..api.client import get_admin_alpha_client
11
+ from ..config.store import get_effective_value
12
+ from ..utils import handle_error, info, output, require_options, resolve_output_format, success
13
+ from ..utils.pagination import paginate_all
14
+
15
+ event_create_rules_app = typer.Typer(
16
+ name="event-create-rules",
17
+ help="Manage event create rules",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+
22
+ def _load_json_config(config_file: str) -> dict:
23
+ """Load and parse a JSON config file."""
24
+ config_path = Path(config_file)
25
+ if not config_path.exists():
26
+ raise typer.BadParameter(f"Config file not found: {config_file}")
27
+ try:
28
+ return json.loads(config_path.read_text())
29
+ except json.JSONDecodeError as exc:
30
+ raise typer.BadParameter(f"Invalid JSON in config file: {exc}")
31
+
32
+
33
+ @event_create_rules_app.command("list")
34
+ def list_cmd(
35
+ property_id: Optional[str] = typer.Option(
36
+ None, "--property-id", "-p", help="Property ID (numeric)"
37
+ ),
38
+ stream_id: str = typer.Option(
39
+ ..., "--stream-id", "-s", help="Data stream ID"
40
+ ),
41
+ output_format: Optional[str] = typer.Option(
42
+ None, "--output", "-o", help="Output format (json, table, compact)"
43
+ ),
44
+ ):
45
+ """List event create rules for a data stream."""
46
+ try:
47
+ effective_property = get_effective_value(property_id, "default_property_id")
48
+ require_options({"property_id": effective_property}, ["property_id"])
49
+ effective_format = resolve_output_format(output_format)
50
+
51
+ admin = get_admin_alpha_client()
52
+ parent = f"properties/{effective_property}/dataStreams/{stream_id}"
53
+ rules = paginate_all(
54
+ lambda **kw: admin.properties()
55
+ .dataStreams()
56
+ .eventCreateRules()
57
+ .list(parent=parent, **kw)
58
+ .execute(),
59
+ "eventCreateRules",
60
+ pageSize=200,
61
+ )
62
+
63
+ output(
64
+ rules,
65
+ effective_format,
66
+ columns=[
67
+ "name",
68
+ "destinationEvent",
69
+ "sourceCopyParameters",
70
+ ],
71
+ headers=[
72
+ "Resource Name",
73
+ "Destination Event",
74
+ "Copy Source Params",
75
+ ],
76
+ )
77
+ except Exception as e:
78
+ handle_error(e)
79
+
80
+
81
+ @event_create_rules_app.command("get")
82
+ def get_cmd(
83
+ property_id: Optional[str] = typer.Option(
84
+ None, "--property-id", "-p", help="Property ID (numeric)"
85
+ ),
86
+ stream_id: str = typer.Option(
87
+ ..., "--stream-id", "-s", help="Data stream ID"
88
+ ),
89
+ rule_id: str = typer.Option(
90
+ ..., "--rule-id", "-r", help="Event create rule ID"
91
+ ),
92
+ output_format: Optional[str] = typer.Option(
93
+ None, "--output", "-o", help="Output format (json, table, compact)"
94
+ ),
95
+ ):
96
+ """Get details for an event create rule."""
97
+ try:
98
+ effective_property = get_effective_value(property_id, "default_property_id")
99
+ require_options({"property_id": effective_property}, ["property_id"])
100
+ effective_format = resolve_output_format(output_format)
101
+
102
+ admin = get_admin_alpha_client()
103
+ resource_name = (
104
+ f"properties/{effective_property}/dataStreams/{stream_id}"
105
+ f"/eventCreateRules/{rule_id}"
106
+ )
107
+ rule = (
108
+ admin.properties()
109
+ .dataStreams()
110
+ .eventCreateRules()
111
+ .get(name=resource_name)
112
+ .execute()
113
+ )
114
+ output(rule, effective_format)
115
+ except Exception as e:
116
+ handle_error(e)
117
+
118
+
119
+ @event_create_rules_app.command("create")
120
+ def create_cmd(
121
+ property_id: Optional[str] = typer.Option(
122
+ None, "--property-id", "-p", help="Property ID (numeric)"
123
+ ),
124
+ stream_id: str = typer.Option(
125
+ ..., "--stream-id", "-s", help="Data stream ID"
126
+ ),
127
+ config_file: str = typer.Option(
128
+ ..., "--config", "-c", help="Path to JSON event create rule config file"
129
+ ),
130
+ output_format: Optional[str] = typer.Option(
131
+ None, "--output", "-o", help="Output format (json, table, compact)"
132
+ ),
133
+ ):
134
+ """Create an event create rule from a JSON config file."""
135
+ try:
136
+ effective_property = get_effective_value(property_id, "default_property_id")
137
+ require_options({"property_id": effective_property}, ["property_id"])
138
+ effective_format = resolve_output_format(output_format)
139
+
140
+ body = _load_json_config(config_file)
141
+
142
+ admin = get_admin_alpha_client()
143
+ parent = f"properties/{effective_property}/dataStreams/{stream_id}"
144
+ rule = (
145
+ admin.properties()
146
+ .dataStreams()
147
+ .eventCreateRules()
148
+ .create(parent=parent, body=body)
149
+ .execute()
150
+ )
151
+ output(rule, effective_format)
152
+ except typer.BadParameter:
153
+ raise
154
+ except Exception as e:
155
+ handle_error(e)
156
+
157
+
158
+ @event_create_rules_app.command("update")
159
+ def update_cmd(
160
+ property_id: Optional[str] = typer.Option(
161
+ None, "--property-id", "-p", help="Property ID (numeric)"
162
+ ),
163
+ stream_id: str = typer.Option(
164
+ ..., "--stream-id", "-s", help="Data stream ID"
165
+ ),
166
+ rule_id: str = typer.Option(
167
+ ..., "--rule-id", "-r", help="Event create rule ID"
168
+ ),
169
+ config_file: str = typer.Option(
170
+ ..., "--config", "-c", help="Path to JSON file with fields to update"
171
+ ),
172
+ output_format: Optional[str] = typer.Option(
173
+ None, "--output", "-o", help="Output format (json, table, compact)"
174
+ ),
175
+ ):
176
+ """Update an event create rule from a JSON config file."""
177
+ try:
178
+ effective_property = get_effective_value(property_id, "default_property_id")
179
+ require_options({"property_id": effective_property}, ["property_id"])
180
+ effective_format = resolve_output_format(output_format)
181
+
182
+ body = _load_json_config(config_file)
183
+
184
+ if not body:
185
+ raise typer.BadParameter("Config file must contain at least one field to update.")
186
+
187
+ update_mask = ",".join(body.keys())
188
+
189
+ admin = get_admin_alpha_client()
190
+ resource_name = (
191
+ f"properties/{effective_property}/dataStreams/{stream_id}"
192
+ f"/eventCreateRules/{rule_id}"
193
+ )
194
+ rule = (
195
+ admin.properties()
196
+ .dataStreams()
197
+ .eventCreateRules()
198
+ .patch(
199
+ name=resource_name,
200
+ body=body,
201
+ updateMask=update_mask,
202
+ )
203
+ .execute()
204
+ )
205
+ output(rule, effective_format)
206
+ except typer.BadParameter:
207
+ raise
208
+ except Exception as e:
209
+ handle_error(e)
210
+
211
+
212
+ @event_create_rules_app.command("delete")
213
+ def delete_cmd(
214
+ property_id: Optional[str] = typer.Option(
215
+ None, "--property-id", "-p", help="Property ID (numeric)"
216
+ ),
217
+ stream_id: str = typer.Option(
218
+ ..., "--stream-id", "-s", help="Data stream ID"
219
+ ),
220
+ rule_id: str = typer.Option(
221
+ ..., "--rule-id", "-r", help="Event create rule ID"
222
+ ),
223
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
224
+ ):
225
+ """Delete an event create rule."""
226
+ try:
227
+ effective_property = get_effective_value(property_id, "default_property_id")
228
+ require_options({"property_id": effective_property}, ["property_id"])
229
+
230
+ if not yes:
231
+ confirmed = questionary.confirm(
232
+ f"Delete event create rule {rule_id}? This cannot be undone."
233
+ ).ask()
234
+ if not confirmed:
235
+ info("Cancelled.")
236
+ raise typer.Exit()
237
+
238
+ admin = get_admin_alpha_client()
239
+ resource_name = (
240
+ f"properties/{effective_property}/dataStreams/{stream_id}"
241
+ f"/eventCreateRules/{rule_id}"
242
+ )
243
+ admin.properties().dataStreams().eventCreateRules().delete(
244
+ name=resource_name
245
+ ).execute()
246
+ success(f"Event create rule {rule_id} deleted.")
247
+ except typer.Exit:
248
+ raise
249
+ except Exception as e:
250
+ handle_error(e)