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,264 @@
|
|
|
1
|
+
"""Reporting data annotation 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
|
+
annotations_app = typer.Typer(
|
|
22
|
+
name="annotations",
|
|
23
|
+
help="Manage reporting data annotations",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@annotations_app.command("list")
|
|
29
|
+
def list_cmd(
|
|
30
|
+
property_id: Optional[str] = typer.Option(
|
|
31
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
32
|
+
),
|
|
33
|
+
output_format: Optional[str] = typer.Option(
|
|
34
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
"""List reporting data annotations for a property."""
|
|
38
|
+
try:
|
|
39
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
40
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
41
|
+
effective_format = resolve_output_format(output_format)
|
|
42
|
+
|
|
43
|
+
admin = get_admin_alpha_client()
|
|
44
|
+
annotations = paginate_all(
|
|
45
|
+
lambda **kw: admin.properties()
|
|
46
|
+
.reportingDataAnnotations()
|
|
47
|
+
.list(parent=f"properties/{effective_property}", **kw)
|
|
48
|
+
.execute(),
|
|
49
|
+
"reportingDataAnnotations",
|
|
50
|
+
pageSize=200,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
output(
|
|
54
|
+
annotations,
|
|
55
|
+
effective_format,
|
|
56
|
+
columns=[
|
|
57
|
+
"name",
|
|
58
|
+
"title",
|
|
59
|
+
"annotationDate",
|
|
60
|
+
"description",
|
|
61
|
+
"color",
|
|
62
|
+
],
|
|
63
|
+
headers=[
|
|
64
|
+
"Resource Name",
|
|
65
|
+
"Title",
|
|
66
|
+
"Date",
|
|
67
|
+
"Description",
|
|
68
|
+
"Color",
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
handle_error(e)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@annotations_app.command("get")
|
|
76
|
+
def get_cmd(
|
|
77
|
+
property_id: Optional[str] = typer.Option(
|
|
78
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
79
|
+
),
|
|
80
|
+
annotation_id: str = typer.Option(
|
|
81
|
+
..., "--annotation-id", "-a", help="Annotation ID"
|
|
82
|
+
),
|
|
83
|
+
output_format: Optional[str] = typer.Option(
|
|
84
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
85
|
+
),
|
|
86
|
+
):
|
|
87
|
+
"""Get details for a reporting data annotation."""
|
|
88
|
+
try:
|
|
89
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
90
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
91
|
+
effective_format = resolve_output_format(output_format)
|
|
92
|
+
|
|
93
|
+
admin = get_admin_alpha_client()
|
|
94
|
+
annotation = (
|
|
95
|
+
admin.properties()
|
|
96
|
+
.reportingDataAnnotations()
|
|
97
|
+
.get(
|
|
98
|
+
name=f"properties/{effective_property}/reportingDataAnnotations/{annotation_id}"
|
|
99
|
+
)
|
|
100
|
+
.execute()
|
|
101
|
+
)
|
|
102
|
+
output(annotation, effective_format)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
handle_error(e)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@annotations_app.command("create")
|
|
108
|
+
def create_cmd(
|
|
109
|
+
property_id: Optional[str] = typer.Option(
|
|
110
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
111
|
+
),
|
|
112
|
+
title: str = typer.Option(..., "--title", help="Annotation title"),
|
|
113
|
+
annotation_date: str = typer.Option(
|
|
114
|
+
..., "--annotation-date", help="Date in YYYY-MM-DD format"
|
|
115
|
+
),
|
|
116
|
+
description: str = typer.Option("", "--description", help="Annotation description"),
|
|
117
|
+
color: Optional[str] = typer.Option(None, "--color", help="Annotation color"),
|
|
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 reporting data annotation."""
|
|
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
|
+
"title": title,
|
|
133
|
+
"annotationDate": annotation_date,
|
|
134
|
+
"description": description,
|
|
135
|
+
}
|
|
136
|
+
if color is not None:
|
|
137
|
+
body["color"] = color
|
|
138
|
+
|
|
139
|
+
if dry_run:
|
|
140
|
+
handle_dry_run("create", "POST", f"properties/{effective_property}", body)
|
|
141
|
+
|
|
142
|
+
admin = get_admin_alpha_client()
|
|
143
|
+
annotation = (
|
|
144
|
+
admin.properties()
|
|
145
|
+
.reportingDataAnnotations()
|
|
146
|
+
.create(parent=f"properties/{effective_property}", body=body)
|
|
147
|
+
.execute()
|
|
148
|
+
)
|
|
149
|
+
output(annotation, effective_format)
|
|
150
|
+
except typer.Exit:
|
|
151
|
+
raise
|
|
152
|
+
except Exception as e:
|
|
153
|
+
handle_error(e)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@annotations_app.command("update")
|
|
157
|
+
def update_cmd(
|
|
158
|
+
property_id: Optional[str] = typer.Option(
|
|
159
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
160
|
+
),
|
|
161
|
+
annotation_id: str = typer.Option(
|
|
162
|
+
..., "--annotation-id", "-a", help="Annotation ID"
|
|
163
|
+
),
|
|
164
|
+
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
165
|
+
description: Optional[str] = typer.Option(None, "--description", help="New description"),
|
|
166
|
+
color: Optional[str] = typer.Option(None, "--color", help="New color"),
|
|
167
|
+
dry_run: bool = typer.Option(
|
|
168
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
169
|
+
),
|
|
170
|
+
output_format: Optional[str] = typer.Option(
|
|
171
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
172
|
+
),
|
|
173
|
+
):
|
|
174
|
+
"""Update a reporting data annotation."""
|
|
175
|
+
try:
|
|
176
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
177
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
178
|
+
effective_format = resolve_output_format(output_format)
|
|
179
|
+
|
|
180
|
+
body = {}
|
|
181
|
+
mask_fields = []
|
|
182
|
+
if title is not None:
|
|
183
|
+
body["title"] = title
|
|
184
|
+
mask_fields.append("title")
|
|
185
|
+
if description is not None:
|
|
186
|
+
body["description"] = description
|
|
187
|
+
mask_fields.append("description")
|
|
188
|
+
if color is not None:
|
|
189
|
+
body["color"] = color
|
|
190
|
+
mask_fields.append("color")
|
|
191
|
+
|
|
192
|
+
if not mask_fields:
|
|
193
|
+
raise typer.BadParameter(
|
|
194
|
+
"At least one field must be specified: --title, --description, --color"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
resource_name = f"properties/{effective_property}/reportingDataAnnotations/{annotation_id}"
|
|
198
|
+
if dry_run:
|
|
199
|
+
handle_dry_run(
|
|
200
|
+
"update", "PATCH", resource_name,
|
|
201
|
+
body, update_mask=",".join(mask_fields),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
admin = get_admin_alpha_client()
|
|
205
|
+
annotation = (
|
|
206
|
+
admin.properties()
|
|
207
|
+
.reportingDataAnnotations()
|
|
208
|
+
.patch(
|
|
209
|
+
name=resource_name,
|
|
210
|
+
body=body,
|
|
211
|
+
updateMask=",".join(mask_fields),
|
|
212
|
+
)
|
|
213
|
+
.execute()
|
|
214
|
+
)
|
|
215
|
+
output(annotation, effective_format)
|
|
216
|
+
except (typer.BadParameter, typer.Exit):
|
|
217
|
+
raise
|
|
218
|
+
except Exception as e:
|
|
219
|
+
handle_error(e)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@annotations_app.command("delete")
|
|
223
|
+
def delete_cmd(
|
|
224
|
+
property_id: Optional[str] = typer.Option(
|
|
225
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
226
|
+
),
|
|
227
|
+
annotation_id: str = typer.Option(
|
|
228
|
+
..., "--annotation-id", "-a", help="Annotation ID"
|
|
229
|
+
),
|
|
230
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
231
|
+
dry_run: bool = typer.Option(
|
|
232
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
233
|
+
),
|
|
234
|
+
):
|
|
235
|
+
"""Delete a reporting data annotation."""
|
|
236
|
+
try:
|
|
237
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
238
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
239
|
+
|
|
240
|
+
if dry_run:
|
|
241
|
+
handle_dry_run(
|
|
242
|
+
"delete", "DELETE",
|
|
243
|
+
f"properties/{effective_property}/reportingDataAnnotations/{annotation_id}",
|
|
244
|
+
None,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not yes:
|
|
248
|
+
confirmed = questionary.confirm(
|
|
249
|
+
f"Delete annotation {annotation_id}? This cannot be undone."
|
|
250
|
+
).ask()
|
|
251
|
+
if not confirmed:
|
|
252
|
+
info("Cancelled.")
|
|
253
|
+
raise typer.Exit()
|
|
254
|
+
|
|
255
|
+
admin = get_admin_alpha_client()
|
|
256
|
+
resource_name = f"properties/{effective_property}/reportingDataAnnotations/{annotation_id}"
|
|
257
|
+
admin.properties().reportingDataAnnotations().delete(
|
|
258
|
+
name=resource_name
|
|
259
|
+
).execute()
|
|
260
|
+
success(f"Annotation {annotation_id} deleted.")
|
|
261
|
+
except typer.Exit:
|
|
262
|
+
raise
|
|
263
|
+
except Exception as e:
|
|
264
|
+
handle_error(e)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Audience 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
|
+
audiences_app = typer.Typer(
|
|
16
|
+
name="audiences",
|
|
17
|
+
help="Manage audiences",
|
|
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
|
+
@audiences_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
|
+
output_format: Optional[str] = typer.Option(
|
|
39
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
40
|
+
),
|
|
41
|
+
):
|
|
42
|
+
"""List audiences for a property."""
|
|
43
|
+
try:
|
|
44
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
45
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
46
|
+
effective_format = resolve_output_format(output_format)
|
|
47
|
+
|
|
48
|
+
admin = get_admin_alpha_client()
|
|
49
|
+
audiences = paginate_all(
|
|
50
|
+
lambda **kw: admin.properties()
|
|
51
|
+
.audiences()
|
|
52
|
+
.list(parent=f"properties/{effective_property}", **kw)
|
|
53
|
+
.execute(),
|
|
54
|
+
"audiences",
|
|
55
|
+
pageSize=200,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
output(
|
|
59
|
+
audiences,
|
|
60
|
+
effective_format,
|
|
61
|
+
columns=[
|
|
62
|
+
"name",
|
|
63
|
+
"displayName",
|
|
64
|
+
"description",
|
|
65
|
+
"membershipDurationDays",
|
|
66
|
+
],
|
|
67
|
+
headers=[
|
|
68
|
+
"Resource Name",
|
|
69
|
+
"Display Name",
|
|
70
|
+
"Description",
|
|
71
|
+
"Membership Days",
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
handle_error(e)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@audiences_app.command("get")
|
|
79
|
+
def get_cmd(
|
|
80
|
+
property_id: Optional[str] = typer.Option(
|
|
81
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
82
|
+
),
|
|
83
|
+
audience_id: str = typer.Option(
|
|
84
|
+
..., "--audience-id", "-a", help="Audience ID"
|
|
85
|
+
),
|
|
86
|
+
output_format: Optional[str] = typer.Option(
|
|
87
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
88
|
+
),
|
|
89
|
+
):
|
|
90
|
+
"""Get details for an audience."""
|
|
91
|
+
try:
|
|
92
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
93
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
94
|
+
effective_format = resolve_output_format(output_format)
|
|
95
|
+
|
|
96
|
+
admin = get_admin_alpha_client()
|
|
97
|
+
audience = (
|
|
98
|
+
admin.properties()
|
|
99
|
+
.audiences()
|
|
100
|
+
.get(
|
|
101
|
+
name=f"properties/{effective_property}/audiences/{audience_id}"
|
|
102
|
+
)
|
|
103
|
+
.execute()
|
|
104
|
+
)
|
|
105
|
+
output(audience, effective_format)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
handle_error(e)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@audiences_app.command("create")
|
|
111
|
+
def create_cmd(
|
|
112
|
+
property_id: Optional[str] = typer.Option(
|
|
113
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
114
|
+
),
|
|
115
|
+
config_file: str = typer.Option(
|
|
116
|
+
..., "--config", "-c", help="Path to JSON audience config file"
|
|
117
|
+
),
|
|
118
|
+
output_format: Optional[str] = typer.Option(
|
|
119
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
120
|
+
),
|
|
121
|
+
):
|
|
122
|
+
"""Create an audience from a JSON config file."""
|
|
123
|
+
try:
|
|
124
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
125
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
126
|
+
effective_format = resolve_output_format(output_format)
|
|
127
|
+
|
|
128
|
+
body = _load_json_config(config_file)
|
|
129
|
+
|
|
130
|
+
admin = get_admin_alpha_client()
|
|
131
|
+
audience = (
|
|
132
|
+
admin.properties()
|
|
133
|
+
.audiences()
|
|
134
|
+
.create(parent=f"properties/{effective_property}", body=body)
|
|
135
|
+
.execute()
|
|
136
|
+
)
|
|
137
|
+
output(audience, effective_format)
|
|
138
|
+
except typer.BadParameter:
|
|
139
|
+
raise
|
|
140
|
+
except Exception as e:
|
|
141
|
+
handle_error(e)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@audiences_app.command("update")
|
|
145
|
+
def update_cmd(
|
|
146
|
+
property_id: Optional[str] = typer.Option(
|
|
147
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
148
|
+
),
|
|
149
|
+
audience_id: str = typer.Option(
|
|
150
|
+
..., "--audience-id", "-a", help="Audience ID"
|
|
151
|
+
),
|
|
152
|
+
config_file: str = typer.Option(
|
|
153
|
+
..., "--config", "-c", help="Path to JSON file with fields to update"
|
|
154
|
+
),
|
|
155
|
+
output_format: Optional[str] = typer.Option(
|
|
156
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
157
|
+
),
|
|
158
|
+
):
|
|
159
|
+
"""Update an audience from a JSON config file."""
|
|
160
|
+
try:
|
|
161
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
162
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
163
|
+
effective_format = resolve_output_format(output_format)
|
|
164
|
+
|
|
165
|
+
body = _load_json_config(config_file)
|
|
166
|
+
|
|
167
|
+
if not body:
|
|
168
|
+
raise typer.BadParameter("Config file must contain at least one field to update.")
|
|
169
|
+
|
|
170
|
+
update_mask = ",".join(body.keys())
|
|
171
|
+
|
|
172
|
+
admin = get_admin_alpha_client()
|
|
173
|
+
resource_name = f"properties/{effective_property}/audiences/{audience_id}"
|
|
174
|
+
audience = (
|
|
175
|
+
admin.properties()
|
|
176
|
+
.audiences()
|
|
177
|
+
.patch(
|
|
178
|
+
name=resource_name,
|
|
179
|
+
body=body,
|
|
180
|
+
updateMask=update_mask,
|
|
181
|
+
)
|
|
182
|
+
.execute()
|
|
183
|
+
)
|
|
184
|
+
output(audience, effective_format)
|
|
185
|
+
except typer.BadParameter:
|
|
186
|
+
raise
|
|
187
|
+
except Exception as e:
|
|
188
|
+
handle_error(e)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@audiences_app.command("archive")
|
|
192
|
+
def archive_cmd(
|
|
193
|
+
property_id: Optional[str] = typer.Option(
|
|
194
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
195
|
+
),
|
|
196
|
+
audience_id: str = typer.Option(
|
|
197
|
+
..., "--audience-id", "-a", help="Audience ID"
|
|
198
|
+
),
|
|
199
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
200
|
+
):
|
|
201
|
+
"""Archive an audience."""
|
|
202
|
+
try:
|
|
203
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
204
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
205
|
+
|
|
206
|
+
if not yes:
|
|
207
|
+
confirmed = questionary.confirm(
|
|
208
|
+
f"Archive audience {audience_id}? This cannot be undone."
|
|
209
|
+
).ask()
|
|
210
|
+
if not confirmed:
|
|
211
|
+
info("Cancelled.")
|
|
212
|
+
raise typer.Exit()
|
|
213
|
+
|
|
214
|
+
admin = get_admin_alpha_client()
|
|
215
|
+
resource_name = f"properties/{effective_property}/audiences/{audience_id}"
|
|
216
|
+
admin.properties().audiences().archive(
|
|
217
|
+
name=resource_name, body={}
|
|
218
|
+
).execute()
|
|
219
|
+
success(f"Audience {audience_id} archived.")
|
|
220
|
+
except typer.Exit:
|
|
221
|
+
raise
|
|
222
|
+
except Exception as e:
|
|
223
|
+
handle_error(e)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Authentication commands: login, logout, status.
|
|
2
|
+
|
|
3
|
+
Equivalent to GTM CLI's commands/auth.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ..api.client import clear_client_cache
|
|
11
|
+
from ..auth.oauth import get_auth_status, login, logout
|
|
12
|
+
from ..auth.service_account import (
|
|
13
|
+
clear_auth_method,
|
|
14
|
+
load_auth_method,
|
|
15
|
+
login_with_service_account,
|
|
16
|
+
)
|
|
17
|
+
from ..utils import error, info, output, success
|
|
18
|
+
|
|
19
|
+
auth_app = typer.Typer(name="auth", help="Manage authentication", no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
_SETUP_GUIDE = r"""# GA CLI — OAuth Credential Setup
|
|
22
|
+
|
|
23
|
+
GA CLI requires your own Google Cloud Platform OAuth credentials.
|
|
24
|
+
Follow these steps to create them.
|
|
25
|
+
|
|
26
|
+
## Step 1: Create or Select a GCP Project
|
|
27
|
+
|
|
28
|
+
1. Go to the Google Cloud Console: https://console.cloud.google.com/
|
|
29
|
+
2. Create a new project or select an existing one
|
|
30
|
+
3. Note your project ID
|
|
31
|
+
|
|
32
|
+
## Step 2: Enable Required APIs
|
|
33
|
+
|
|
34
|
+
In the GCP Console, go to APIs & Services > Library and enable:
|
|
35
|
+
- Google Analytics Admin API
|
|
36
|
+
- Google Analytics Data API
|
|
37
|
+
|
|
38
|
+
Or via gcloud CLI:
|
|
39
|
+
|
|
40
|
+
gcloud services enable analyticsadmin.googleapis.com analyticsdata.googleapis.com
|
|
41
|
+
|
|
42
|
+
## Step 3: Configure OAuth Consent Screen
|
|
43
|
+
|
|
44
|
+
1. Go to APIs & Services > OAuth consent screen
|
|
45
|
+
2. Choose "External" user type (or "Internal" if using Google Workspace)
|
|
46
|
+
3. Fill in the required fields: app name, user support email, developer contact
|
|
47
|
+
4. No scopes need to be added manually — GA CLI requests them at login time
|
|
48
|
+
5. For personal use, leave the app in "Testing" mode — it works for the
|
|
49
|
+
project owner and up to 100 added test users without Google verification
|
|
50
|
+
|
|
51
|
+
## Step 4: Create OAuth Client ID
|
|
52
|
+
|
|
53
|
+
1. Go to APIs & Services > Credentials
|
|
54
|
+
2. Click "Create Credentials" > "OAuth client ID"
|
|
55
|
+
3. Choose "Desktop app" as the application type
|
|
56
|
+
4. Give it a name (e.g., "GA CLI")
|
|
57
|
+
5. Click "Create" and download the JSON file
|
|
58
|
+
|
|
59
|
+
## Step 5: Provide Credentials to GA CLI
|
|
60
|
+
|
|
61
|
+
Option A — Place the downloaded JSON file (recommended):
|
|
62
|
+
|
|
63
|
+
mkdir -p ~/.config/ga-cli
|
|
64
|
+
cp /path/to/downloaded/client_secret_*.json ~/.config/ga-cli/client_secret.json
|
|
65
|
+
|
|
66
|
+
Option B — Set environment variables:
|
|
67
|
+
|
|
68
|
+
export GA_CLI_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
69
|
+
export GA_CLI_CLIENT_SECRET="your-client-secret"
|
|
70
|
+
|
|
71
|
+
## Step 6: Authenticate
|
|
72
|
+
|
|
73
|
+
ga auth login
|
|
74
|
+
|
|
75
|
+
This opens your browser for Google OAuth consent and stores the token
|
|
76
|
+
locally at ~/.config/ga-cli/credentials.json.
|
|
77
|
+
|
|
78
|
+
## Verification
|
|
79
|
+
|
|
80
|
+
ga auth status # Check authentication state
|
|
81
|
+
ga accounts list # Verify API access
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- "Testing" mode is sufficient for personal use — no Google verification needed
|
|
86
|
+
- For team use, publish the consent screen to "Production" within your GCP project
|
|
87
|
+
- Service account auth (ga auth login --service-account /path/key.json) does not
|
|
88
|
+
require OAuth credentials and works independently
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@auth_app.command("setup")
|
|
93
|
+
def setup_cmd():
|
|
94
|
+
"""Show step-by-step instructions for obtaining GCP OAuth credentials."""
|
|
95
|
+
print(_SETUP_GUIDE)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@auth_app.command("login")
|
|
99
|
+
def login_cmd(
|
|
100
|
+
service_account: Optional[str] = typer.Option(
|
|
101
|
+
None, "--service-account", "-s",
|
|
102
|
+
help="Path to service account key JSON file",
|
|
103
|
+
),
|
|
104
|
+
):
|
|
105
|
+
"""Authenticate with Google Analytics."""
|
|
106
|
+
try:
|
|
107
|
+
if service_account:
|
|
108
|
+
info(f"Authenticating with service account: {service_account}")
|
|
109
|
+
email = login_with_service_account(service_account)
|
|
110
|
+
success(f"Authenticated as {email}")
|
|
111
|
+
info("Service account credentials are now active.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# OAuth (default)
|
|
115
|
+
status = get_auth_status()
|
|
116
|
+
if status.get("authenticated") and status.get("email"):
|
|
117
|
+
info(f"Already authenticated as {status['email']}")
|
|
118
|
+
info("Run 'ga auth logout' first, or 'ga auth status' to view details.")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
info("Opening browser for authentication...")
|
|
122
|
+
login()
|
|
123
|
+
clear_client_cache()
|
|
124
|
+
|
|
125
|
+
# Fetch user info to display
|
|
126
|
+
status = get_auth_status()
|
|
127
|
+
email = status.get("email", "unknown")
|
|
128
|
+
success(f"Authenticated as {email}")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
error(f"Authentication failed: {e}")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@auth_app.command("logout")
|
|
135
|
+
def logout_cmd():
|
|
136
|
+
"""Sign out and revoke access tokens."""
|
|
137
|
+
try:
|
|
138
|
+
auth_method = load_auth_method()
|
|
139
|
+
status = get_auth_status()
|
|
140
|
+
|
|
141
|
+
if not auth_method and not status.get("authenticated"):
|
|
142
|
+
info("Not currently authenticated.")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if auth_method and auth_method.get("method") == "service-account":
|
|
146
|
+
clear_auth_method()
|
|
147
|
+
email = auth_method.get("service_account_email", "")
|
|
148
|
+
success(f"Cleared service account configuration ({email})")
|
|
149
|
+
info("The service account key file was not deleted.")
|
|
150
|
+
|
|
151
|
+
if status.get("authenticated"):
|
|
152
|
+
logout()
|
|
153
|
+
clear_client_cache()
|
|
154
|
+
success("Logged out from OAuth session.")
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
error(f"Logout failed: {e}")
|
|
158
|
+
raise typer.Exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@auth_app.command("status")
|
|
162
|
+
def status_cmd(
|
|
163
|
+
output_format: str = typer.Option("table", "--output", "-o", help="Output format"),
|
|
164
|
+
):
|
|
165
|
+
"""Show current authentication status."""
|
|
166
|
+
try:
|
|
167
|
+
import os
|
|
168
|
+
|
|
169
|
+
# Check for env var override (same priority as GTM CLI)
|
|
170
|
+
env_key = os.environ.get("GA_CLI_SERVICE_ACCOUNT") or os.environ.get(
|
|
171
|
+
"GOOGLE_APPLICATION_CREDENTIALS"
|
|
172
|
+
)
|
|
173
|
+
if env_key:
|
|
174
|
+
data = {
|
|
175
|
+
"authenticated": True,
|
|
176
|
+
"method": "service-account",
|
|
177
|
+
"source": "environment variable",
|
|
178
|
+
"key_path": env_key,
|
|
179
|
+
}
|
|
180
|
+
output(data, output_format)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Check saved auth method
|
|
184
|
+
auth_method = load_auth_method()
|
|
185
|
+
if auth_method and auth_method.get("method") == "service-account":
|
|
186
|
+
data = {
|
|
187
|
+
"authenticated": True,
|
|
188
|
+
"method": "service-account",
|
|
189
|
+
"email": auth_method.get("service_account_email"),
|
|
190
|
+
"key_path": auth_method.get("service_account_path"),
|
|
191
|
+
}
|
|
192
|
+
output(data, output_format)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Fall back to OAuth
|
|
196
|
+
auth_status = get_auth_status()
|
|
197
|
+
auth_status["method"] = "oauth"
|
|
198
|
+
output(auth_status, output_format)
|
|
199
|
+
|
|
200
|
+
if not auth_status.get("authenticated"):
|
|
201
|
+
info("Run 'ga auth login' to authenticate.")
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
error(f"Failed to get status: {e}")
|
|
205
|
+
raise typer.Exit(1)
|