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.
- ga_cli/__init__.py +8 -0
- ga_cli/api/__init__.py +0 -0
- ga_cli/api/client.py +142 -0
- ga_cli/auth/__init__.py +35 -0
- ga_cli/auth/credentials.py +126 -0
- ga_cli/auth/oauth.py +322 -0
- ga_cli/auth/service_account.py +155 -0
- ga_cli/commands/__init__.py +0 -0
- ga_cli/commands/access_bindings.py +254 -0
- ga_cli/commands/access_reports.py +201 -0
- ga_cli/commands/account_summaries.py +68 -0
- ga_cli/commands/accounts.py +297 -0
- ga_cli/commands/agent_cmd.py +776 -0
- ga_cli/commands/annotations.py +264 -0
- ga_cli/commands/audiences.py +223 -0
- ga_cli/commands/auth_cmd.py +205 -0
- ga_cli/commands/bigquery_links.py +309 -0
- ga_cli/commands/calculated_metrics.py +312 -0
- ga_cli/commands/channel_groups.py +223 -0
- ga_cli/commands/completions_cmd.py +55 -0
- ga_cli/commands/config_cmd.py +113 -0
- ga_cli/commands/custom_dimensions.py +272 -0
- ga_cli/commands/custom_metrics.py +305 -0
- ga_cli/commands/data_retention.py +153 -0
- ga_cli/commands/data_streams.py +277 -0
- ga_cli/commands/event_create_rules.py +250 -0
- ga_cli/commands/event_edit_rules.py +292 -0
- ga_cli/commands/firebase_links.py +142 -0
- ga_cli/commands/google_ads_links.py +225 -0
- ga_cli/commands/key_events.py +269 -0
- ga_cli/commands/mp_secrets.py +265 -0
- ga_cli/commands/properties.py +330 -0
- ga_cli/commands/property_settings.py +287 -0
- ga_cli/commands/reports.py +726 -0
- ga_cli/commands/upgrade_cmd.py +148 -0
- ga_cli/config/__init__.py +0 -0
- ga_cli/config/constants.py +61 -0
- ga_cli/config/store.py +115 -0
- ga_cli/main.py +110 -0
- ga_cli/utils/__init__.py +20 -0
- ga_cli/utils/describe.py +129 -0
- ga_cli/utils/dry_run.py +40 -0
- ga_cli/utils/errors.py +150 -0
- ga_cli/utils/output.py +209 -0
- ga_cli/utils/pagination.py +93 -0
- google_analytics_cli-0.1.0.dist-info/METADATA +321 -0
- google_analytics_cli-0.1.0.dist-info/RECORD +49 -0
- google_analytics_cli-0.1.0.dist-info/WHEEL +4 -0
- 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)
|