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,309 @@
1
+ """BigQuery link management commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import questionary
6
+ import typer
7
+
8
+ from ..api.client import get_admin_alpha_client
9
+ from ..config.store import get_effective_value
10
+ from ..utils import handle_error, info, output, require_options, resolve_output_format, success
11
+ from ..utils.pagination import paginate_all
12
+
13
+ bigquery_links_app = typer.Typer(
14
+ name="bigquery-links",
15
+ help="Manage BigQuery links",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+
20
+ def _normalize_project(project: str) -> str:
21
+ """Ensure project is in 'projects/{id}' format."""
22
+ if project.startswith("projects/"):
23
+ return project
24
+ return f"projects/{project}"
25
+
26
+
27
+ @bigquery_links_app.command("list")
28
+ def list_cmd(
29
+ property_id: Optional[str] = typer.Option(
30
+ None, "--property-id", "-p", help="Property ID (numeric)"
31
+ ),
32
+ output_format: Optional[str] = typer.Option(
33
+ None, "--output", "-o", help="Output format (json, table, compact)"
34
+ ),
35
+ ):
36
+ """List BigQuery links for a property."""
37
+ try:
38
+ effective_property = get_effective_value(property_id, "default_property_id")
39
+ require_options({"property_id": effective_property}, ["property_id"])
40
+ effective_format = resolve_output_format(output_format)
41
+
42
+ admin = get_admin_alpha_client()
43
+ links = paginate_all(
44
+ lambda **kw: admin.properties()
45
+ .bigQueryLinks()
46
+ .list(parent=f"properties/{effective_property}", **kw)
47
+ .execute(),
48
+ "bigqueryLinks",
49
+ pageSize=200,
50
+ )
51
+
52
+ output(
53
+ links,
54
+ effective_format,
55
+ columns=[
56
+ "name",
57
+ "project",
58
+ "datasetLocation",
59
+ "dailyExportEnabled",
60
+ "streamingExportEnabled",
61
+ "createTime",
62
+ ],
63
+ headers=[
64
+ "Resource Name",
65
+ "Project",
66
+ "Dataset Location",
67
+ "Daily Export",
68
+ "Streaming Export",
69
+ "Create Time",
70
+ ],
71
+ )
72
+ except Exception as e:
73
+ handle_error(e)
74
+
75
+
76
+ @bigquery_links_app.command("get")
77
+ def get_cmd(
78
+ property_id: Optional[str] = typer.Option(
79
+ None, "--property-id", "-p", help="Property ID (numeric)"
80
+ ),
81
+ link_id: str = typer.Option(
82
+ ..., "--link-id", "-l", help="BigQuery link ID"
83
+ ),
84
+ output_format: Optional[str] = typer.Option(
85
+ None, "--output", "-o", help="Output format (json, table, compact)"
86
+ ),
87
+ ):
88
+ """Get details for a BigQuery link."""
89
+ try:
90
+ effective_property = get_effective_value(property_id, "default_property_id")
91
+ require_options({"property_id": effective_property}, ["property_id"])
92
+ effective_format = resolve_output_format(output_format)
93
+
94
+ admin = get_admin_alpha_client()
95
+ link = (
96
+ admin.properties()
97
+ .bigQueryLinks()
98
+ .get(
99
+ name=f"properties/{effective_property}/bigQueryLinks/{link_id}"
100
+ )
101
+ .execute()
102
+ )
103
+ output(link, effective_format)
104
+ except Exception as e:
105
+ handle_error(e)
106
+
107
+
108
+ @bigquery_links_app.command("create")
109
+ def create_cmd(
110
+ property_id: Optional[str] = typer.Option(
111
+ None, "--property-id", "-p", help="Property ID (numeric)"
112
+ ),
113
+ project: str = typer.Option(
114
+ ..., "--project", help="Google Cloud project (number or ID)"
115
+ ),
116
+ dataset_location: str = typer.Option(
117
+ ..., "--dataset-location", help="BigQuery dataset location (e.g., US, EU)"
118
+ ),
119
+ daily_export: Optional[bool] = typer.Option(
120
+ None, "--daily-export/--no-daily-export", help="Enable daily export"
121
+ ),
122
+ streaming_export: Optional[bool] = typer.Option(
123
+ None, "--streaming-export/--no-streaming-export", help="Enable streaming export"
124
+ ),
125
+ fresh_daily_export: Optional[bool] = typer.Option(
126
+ None, "--fresh-daily-export/--no-fresh-daily-export", help="Enable fresh daily export"
127
+ ),
128
+ include_advertising_id: Optional[bool] = typer.Option(
129
+ None,
130
+ "--include-advertising-id/--no-include-advertising-id",
131
+ help="Include advertising identifiers for mobile app streams",
132
+ ),
133
+ export_streams: Optional[str] = typer.Option(
134
+ None, "--export-streams", help="Comma-separated data stream IDs to export"
135
+ ),
136
+ excluded_events: Optional[str] = typer.Option(
137
+ None, "--excluded-events", help="Comma-separated event names to exclude"
138
+ ),
139
+ output_format: Optional[str] = typer.Option(
140
+ None, "--output", "-o", help="Output format (json, table, compact)"
141
+ ),
142
+ ):
143
+ """Create a BigQuery link."""
144
+ try:
145
+ effective_property = get_effective_value(property_id, "default_property_id")
146
+ require_options({"property_id": effective_property}, ["property_id"])
147
+ effective_format = resolve_output_format(output_format)
148
+
149
+ body = {
150
+ "project": _normalize_project(project),
151
+ "datasetLocation": dataset_location,
152
+ }
153
+ if daily_export is not None:
154
+ body["dailyExportEnabled"] = daily_export
155
+ if streaming_export is not None:
156
+ body["streamingExportEnabled"] = streaming_export
157
+ if fresh_daily_export is not None:
158
+ body["freshDailyExportEnabled"] = fresh_daily_export
159
+ if include_advertising_id is not None:
160
+ body["includeAdvertisingId"] = include_advertising_id
161
+ if export_streams is not None:
162
+ body["exportStreams"] = [
163
+ f"properties/{effective_property}/dataStreams/{sid.strip()}"
164
+ for sid in export_streams.split(",")
165
+ ]
166
+ if excluded_events is not None:
167
+ body["excludedEvents"] = [
168
+ e.strip() for e in excluded_events.split(",")
169
+ ]
170
+
171
+ admin = get_admin_alpha_client()
172
+ link = (
173
+ admin.properties()
174
+ .bigQueryLinks()
175
+ .create(parent=f"properties/{effective_property}", body=body)
176
+ .execute()
177
+ )
178
+ output(link, effective_format)
179
+ except Exception as e:
180
+ handle_error(e)
181
+
182
+
183
+ @bigquery_links_app.command("update")
184
+ def update_cmd(
185
+ property_id: Optional[str] = typer.Option(
186
+ None, "--property-id", "-p", help="Property ID (numeric)"
187
+ ),
188
+ link_id: str = typer.Option(
189
+ ..., "--link-id", "-l", help="BigQuery link ID"
190
+ ),
191
+ daily_export: Optional[bool] = typer.Option(
192
+ None, "--daily-export/--no-daily-export", help="Enable daily export"
193
+ ),
194
+ streaming_export: Optional[bool] = typer.Option(
195
+ None, "--streaming-export/--no-streaming-export", help="Enable streaming export"
196
+ ),
197
+ fresh_daily_export: Optional[bool] = typer.Option(
198
+ None, "--fresh-daily-export/--no-fresh-daily-export", help="Enable fresh daily export"
199
+ ),
200
+ include_advertising_id: Optional[bool] = typer.Option(
201
+ None,
202
+ "--include-advertising-id/--no-include-advertising-id",
203
+ help="Include advertising identifiers for mobile app streams",
204
+ ),
205
+ export_streams: Optional[str] = typer.Option(
206
+ None, "--export-streams", help="Comma-separated data stream IDs to export"
207
+ ),
208
+ excluded_events: Optional[str] = typer.Option(
209
+ None, "--excluded-events", help="Comma-separated event names to exclude"
210
+ ),
211
+ output_format: Optional[str] = typer.Option(
212
+ None, "--output", "-o", help="Output format (json, table, compact)"
213
+ ),
214
+ ):
215
+ """Update a BigQuery link."""
216
+ try:
217
+ effective_property = get_effective_value(property_id, "default_property_id")
218
+ require_options({"property_id": effective_property}, ["property_id"])
219
+ effective_format = resolve_output_format(output_format)
220
+
221
+ body = {}
222
+ mask_fields = []
223
+ if daily_export is not None:
224
+ body["dailyExportEnabled"] = daily_export
225
+ mask_fields.append("dailyExportEnabled")
226
+ if streaming_export is not None:
227
+ body["streamingExportEnabled"] = streaming_export
228
+ mask_fields.append("streamingExportEnabled")
229
+ if fresh_daily_export is not None:
230
+ body["freshDailyExportEnabled"] = fresh_daily_export
231
+ mask_fields.append("freshDailyExportEnabled")
232
+ if include_advertising_id is not None:
233
+ body["includeAdvertisingId"] = include_advertising_id
234
+ mask_fields.append("includeAdvertisingId")
235
+ if export_streams is not None:
236
+ body["exportStreams"] = [
237
+ f"properties/{effective_property}/dataStreams/{sid.strip()}"
238
+ for sid in export_streams.split(",")
239
+ ]
240
+ mask_fields.append("exportStreams")
241
+ if excluded_events is not None:
242
+ body["excludedEvents"] = [
243
+ e.strip() for e in excluded_events.split(",")
244
+ ]
245
+ mask_fields.append("excludedEvents")
246
+
247
+ if not mask_fields:
248
+ raise typer.BadParameter(
249
+ "At least one field must be specified: "
250
+ "--daily-export, --streaming-export, --fresh-daily-export, "
251
+ "--include-advertising-id, --export-streams, --excluded-events"
252
+ )
253
+
254
+ admin = get_admin_alpha_client()
255
+ resource_name = (
256
+ f"properties/{effective_property}/bigQueryLinks/{link_id}"
257
+ )
258
+ link = (
259
+ admin.properties()
260
+ .bigQueryLinks()
261
+ .patch(
262
+ name=resource_name,
263
+ body=body,
264
+ updateMask=",".join(mask_fields),
265
+ )
266
+ .execute()
267
+ )
268
+ output(link, effective_format)
269
+ except typer.BadParameter:
270
+ raise
271
+ except Exception as e:
272
+ handle_error(e)
273
+
274
+
275
+ @bigquery_links_app.command("delete")
276
+ def delete_cmd(
277
+ property_id: Optional[str] = typer.Option(
278
+ None, "--property-id", "-p", help="Property ID (numeric)"
279
+ ),
280
+ link_id: str = typer.Option(
281
+ ..., "--link-id", "-l", help="BigQuery link ID"
282
+ ),
283
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
284
+ ):
285
+ """Delete a BigQuery link."""
286
+ try:
287
+ effective_property = get_effective_value(property_id, "default_property_id")
288
+ require_options({"property_id": effective_property}, ["property_id"])
289
+
290
+ if not yes:
291
+ confirmed = questionary.confirm(
292
+ f"Delete BigQuery link {link_id}? This cannot be undone."
293
+ ).ask()
294
+ if not confirmed:
295
+ info("Cancelled.")
296
+ raise typer.Exit()
297
+
298
+ admin = get_admin_alpha_client()
299
+ resource_name = (
300
+ f"properties/{effective_property}/bigQueryLinks/{link_id}"
301
+ )
302
+ admin.properties().bigQueryLinks().delete(
303
+ name=resource_name
304
+ ).execute()
305
+ success(f"BigQuery link {link_id} deleted.")
306
+ except typer.Exit:
307
+ raise
308
+ except Exception as e:
309
+ handle_error(e)
@@ -0,0 +1,312 @@
1
+ """Calculated metric management commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import questionary
6
+ import typer
7
+
8
+ from ..api.client import get_admin_alpha_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
+ calculated_metrics_app = typer.Typer(
22
+ name="calculated-metrics",
23
+ help="Manage calculated metrics",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+ _VALID_METRIC_UNITS = (
28
+ "STANDARD",
29
+ "CURRENCY",
30
+ "FEET",
31
+ "METERS",
32
+ "KILOMETERS",
33
+ "MILES",
34
+ "MILLISECONDS",
35
+ "SECONDS",
36
+ "MINUTES",
37
+ "HOURS",
38
+ )
39
+
40
+
41
+ @calculated_metrics_app.command("list")
42
+ def list_cmd(
43
+ property_id: Optional[str] = typer.Option(
44
+ None, "--property-id", "-p", help="Property ID (numeric)"
45
+ ),
46
+ output_format: Optional[str] = typer.Option(
47
+ None, "--output", "-o", help="Output format (json, table, compact)"
48
+ ),
49
+ ):
50
+ """List calculated metrics for a property."""
51
+ try:
52
+ effective_property = get_effective_value(property_id, "default_property_id")
53
+ require_options({"property_id": effective_property}, ["property_id"])
54
+ effective_format = resolve_output_format(output_format)
55
+
56
+ admin = get_admin_alpha_client()
57
+ metrics = paginate_all(
58
+ lambda **kw: admin.properties()
59
+ .calculatedMetrics()
60
+ .list(parent=f"properties/{effective_property}", **kw)
61
+ .execute(),
62
+ "calculatedMetrics",
63
+ pageSize=200,
64
+ )
65
+
66
+ output(
67
+ metrics,
68
+ effective_format,
69
+ columns=[
70
+ "name",
71
+ "calculatedMetricId",
72
+ "displayName",
73
+ "formula",
74
+ "metricUnit",
75
+ ],
76
+ headers=[
77
+ "Resource Name",
78
+ "Metric ID",
79
+ "Display Name",
80
+ "Formula",
81
+ "Metric Unit",
82
+ ],
83
+ )
84
+ except Exception as e:
85
+ handle_error(e)
86
+
87
+
88
+ @calculated_metrics_app.command("get")
89
+ def get_cmd(
90
+ property_id: Optional[str] = typer.Option(
91
+ None, "--property-id", "-p", help="Property ID (numeric)"
92
+ ),
93
+ metric_id: str = typer.Option(
94
+ ..., "--metric-id", "-m", help="Calculated metric ID"
95
+ ),
96
+ output_format: Optional[str] = typer.Option(
97
+ None, "--output", "-o", help="Output format (json, table, compact)"
98
+ ),
99
+ ):
100
+ """Get details for a calculated metric."""
101
+ try:
102
+ effective_property = get_effective_value(property_id, "default_property_id")
103
+ require_options({"property_id": effective_property}, ["property_id"])
104
+ effective_format = resolve_output_format(output_format)
105
+
106
+ admin = get_admin_alpha_client()
107
+ metric = (
108
+ admin.properties()
109
+ .calculatedMetrics()
110
+ .get(
111
+ name=f"properties/{effective_property}/calculatedMetrics/{metric_id}"
112
+ )
113
+ .execute()
114
+ )
115
+ output(metric, effective_format)
116
+ except Exception as e:
117
+ handle_error(e)
118
+
119
+
120
+ @calculated_metrics_app.command("create")
121
+ def create_cmd(
122
+ property_id: Optional[str] = typer.Option(
123
+ None, "--property-id", "-p", help="Property ID (numeric)"
124
+ ),
125
+ calculated_metric_id: str = typer.Option(
126
+ ..., "--calculated-metric-id", help="Unique metric ID (e.g., 'revenuePerUser')"
127
+ ),
128
+ display_name: str = typer.Option(..., "--display-name", help="Display name"),
129
+ formula: str = typer.Option(
130
+ ..., "--formula", help='Formula (e.g., "{{totalRevenue}} / {{totalUsers}}")'
131
+ ),
132
+ metric_unit: str = typer.Option(
133
+ ..., "--metric-unit", help="Metric unit (STANDARD, CURRENCY, etc.)"
134
+ ),
135
+ description: str = typer.Option("", "--description", help="Description"),
136
+ dry_run: bool = typer.Option(
137
+ False, "--dry-run", help="Preview the request without executing"
138
+ ),
139
+ output_format: Optional[str] = typer.Option(
140
+ None, "--output", "-o", help="Output format (json, table, compact)"
141
+ ),
142
+ ):
143
+ """Create a calculated metric."""
144
+ try:
145
+ effective_property = get_effective_value(property_id, "default_property_id")
146
+ require_options({"property_id": effective_property}, ["property_id"])
147
+ effective_format = resolve_output_format(output_format)
148
+
149
+ unit_upper = metric_unit.upper()
150
+ if unit_upper not in _VALID_METRIC_UNITS:
151
+ raise typer.BadParameter(
152
+ f"Invalid metric unit '{metric_unit}'. "
153
+ f"Must be one of: {', '.join(_VALID_METRIC_UNITS)}"
154
+ )
155
+
156
+ body = {
157
+ "displayName": display_name,
158
+ "description": description,
159
+ "formula": formula,
160
+ "metricUnit": unit_upper,
161
+ }
162
+ if dry_run:
163
+ handle_dry_run("create", "POST", f"properties/{effective_property}", body)
164
+
165
+ admin = get_admin_alpha_client()
166
+ metric = (
167
+ admin.properties()
168
+ .calculatedMetrics()
169
+ .create(
170
+ parent=f"properties/{effective_property}",
171
+ calculatedMetricId=calculated_metric_id,
172
+ body=body,
173
+ )
174
+ .execute()
175
+ )
176
+ output(metric, effective_format)
177
+ except (typer.BadParameter, typer.Exit):
178
+ raise
179
+ except Exception as e:
180
+ handle_error(e)
181
+
182
+
183
+ @calculated_metrics_app.command("update")
184
+ def update_cmd(
185
+ property_id: Optional[str] = typer.Option(
186
+ None, "--property-id", "-p", help="Property ID (numeric)"
187
+ ),
188
+ metric_id: str = typer.Option(
189
+ ..., "--metric-id", "-m", help="Calculated metric ID"
190
+ ),
191
+ display_name: Optional[str] = typer.Option(
192
+ None, "--display-name", help="New display name"
193
+ ),
194
+ description: Optional[str] = typer.Option(
195
+ None, "--description", help="New description"
196
+ ),
197
+ formula: Optional[str] = typer.Option(None, "--formula", help="New formula"),
198
+ metric_unit: Optional[str] = typer.Option(
199
+ None, "--metric-unit", help="New metric unit"
200
+ ),
201
+ dry_run: bool = typer.Option(
202
+ False, "--dry-run", help="Preview the request without executing"
203
+ ),
204
+ output_format: Optional[str] = typer.Option(
205
+ None, "--output", "-o", help="Output format (json, table, compact)"
206
+ ),
207
+ ):
208
+ """Update a calculated metric."""
209
+ try:
210
+ effective_property = get_effective_value(property_id, "default_property_id")
211
+ require_options({"property_id": effective_property}, ["property_id"])
212
+ effective_format = resolve_output_format(output_format)
213
+
214
+ body = {}
215
+ mask_fields = []
216
+ if display_name is not None:
217
+ body["displayName"] = display_name
218
+ mask_fields.append("displayName")
219
+ if description is not None:
220
+ body["description"] = description
221
+ mask_fields.append("description")
222
+ if formula is not None:
223
+ body["formula"] = formula
224
+ mask_fields.append("formula")
225
+ if metric_unit is not None:
226
+ unit_upper = metric_unit.upper()
227
+ if unit_upper not in _VALID_METRIC_UNITS:
228
+ raise typer.BadParameter(
229
+ f"Invalid metric unit '{metric_unit}'. "
230
+ f"Must be one of: {', '.join(_VALID_METRIC_UNITS)}"
231
+ )
232
+ body["metricUnit"] = unit_upper
233
+ mask_fields.append("metricUnit")
234
+
235
+ if not mask_fields:
236
+ raise typer.BadParameter(
237
+ "At least one field must be specified: "
238
+ "--display-name, --description, --formula, --metric-unit"
239
+ )
240
+
241
+ resource_name = (
242
+ f"properties/{effective_property}/calculatedMetrics/{metric_id}"
243
+ )
244
+ if dry_run:
245
+ handle_dry_run(
246
+ "update", "PATCH", resource_name,
247
+ body, update_mask=",".join(mask_fields),
248
+ )
249
+
250
+ admin = get_admin_alpha_client()
251
+ metric = (
252
+ admin.properties()
253
+ .calculatedMetrics()
254
+ .patch(
255
+ name=resource_name,
256
+ body=body,
257
+ updateMask=",".join(mask_fields),
258
+ )
259
+ .execute()
260
+ )
261
+ output(metric, effective_format)
262
+ except (typer.BadParameter, typer.Exit):
263
+ raise
264
+ except Exception as e:
265
+ handle_error(e)
266
+
267
+
268
+ @calculated_metrics_app.command("delete")
269
+ def delete_cmd(
270
+ property_id: Optional[str] = typer.Option(
271
+ None, "--property-id", "-p", help="Property ID (numeric)"
272
+ ),
273
+ metric_id: str = typer.Option(
274
+ ..., "--metric-id", "-m", help="Calculated metric ID"
275
+ ),
276
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
277
+ dry_run: bool = typer.Option(
278
+ False, "--dry-run", help="Preview the request without executing"
279
+ ),
280
+ ):
281
+ """Delete a calculated metric."""
282
+ try:
283
+ effective_property = get_effective_value(property_id, "default_property_id")
284
+ require_options({"property_id": effective_property}, ["property_id"])
285
+
286
+ if dry_run:
287
+ handle_dry_run(
288
+ "delete", "DELETE",
289
+ f"properties/{effective_property}/calculatedMetrics/{metric_id}",
290
+ None,
291
+ )
292
+
293
+ if not yes:
294
+ confirmed = questionary.confirm(
295
+ f"Delete calculated metric {metric_id}? This cannot be undone."
296
+ ).ask()
297
+ if not confirmed:
298
+ info("Cancelled.")
299
+ raise typer.Exit()
300
+
301
+ admin = get_admin_alpha_client()
302
+ resource_name = (
303
+ f"properties/{effective_property}/calculatedMetrics/{metric_id}"
304
+ )
305
+ admin.properties().calculatedMetrics().delete(
306
+ name=resource_name
307
+ ).execute()
308
+ success(f"Calculated metric {metric_id} deleted.")
309
+ except typer.Exit:
310
+ raise
311
+ except Exception as e:
312
+ handle_error(e)