google-analytics-cli 0.1.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.0rc1.dist-info/METADATA +269 -0
- google_analytics_cli-0.1.0rc1.dist-info/RECORD +49 -0
- google_analytics_cli-0.1.0rc1.dist-info/WHEEL +4 -0
- google_analytics_cli-0.1.0rc1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Channel group 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
|
+
channel_groups_app = typer.Typer(
|
|
16
|
+
name="channel-groups",
|
|
17
|
+
help="Manage channel groups",
|
|
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
|
+
@channel_groups_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 channel groups 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
|
+
groups = paginate_all(
|
|
50
|
+
lambda **kw: admin.properties()
|
|
51
|
+
.channelGroups()
|
|
52
|
+
.list(parent=f"properties/{effective_property}", **kw)
|
|
53
|
+
.execute(),
|
|
54
|
+
"channelGroups",
|
|
55
|
+
pageSize=200,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
output(
|
|
59
|
+
groups,
|
|
60
|
+
effective_format,
|
|
61
|
+
columns=[
|
|
62
|
+
"name",
|
|
63
|
+
"displayName",
|
|
64
|
+
"description",
|
|
65
|
+
"systemDefined",
|
|
66
|
+
],
|
|
67
|
+
headers=[
|
|
68
|
+
"Resource Name",
|
|
69
|
+
"Display Name",
|
|
70
|
+
"Description",
|
|
71
|
+
"System Defined",
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
handle_error(e)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@channel_groups_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
|
+
channel_group_id: str = typer.Option(
|
|
84
|
+
..., "--channel-group-id", "-g", help="Channel group 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 a channel group."""
|
|
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
|
+
group = (
|
|
98
|
+
admin.properties()
|
|
99
|
+
.channelGroups()
|
|
100
|
+
.get(
|
|
101
|
+
name=f"properties/{effective_property}/channelGroups/{channel_group_id}"
|
|
102
|
+
)
|
|
103
|
+
.execute()
|
|
104
|
+
)
|
|
105
|
+
output(group, effective_format)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
handle_error(e)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@channel_groups_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 channel group config file"
|
|
117
|
+
),
|
|
118
|
+
output_format: Optional[str] = typer.Option(
|
|
119
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
120
|
+
),
|
|
121
|
+
):
|
|
122
|
+
"""Create a channel group 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
|
+
group = (
|
|
132
|
+
admin.properties()
|
|
133
|
+
.channelGroups()
|
|
134
|
+
.create(parent=f"properties/{effective_property}", body=body)
|
|
135
|
+
.execute()
|
|
136
|
+
)
|
|
137
|
+
output(group, effective_format)
|
|
138
|
+
except typer.BadParameter:
|
|
139
|
+
raise
|
|
140
|
+
except Exception as e:
|
|
141
|
+
handle_error(e)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@channel_groups_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
|
+
channel_group_id: str = typer.Option(
|
|
150
|
+
..., "--channel-group-id", "-g", help="Channel group 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 a channel group 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}/channelGroups/{channel_group_id}"
|
|
174
|
+
group = (
|
|
175
|
+
admin.properties()
|
|
176
|
+
.channelGroups()
|
|
177
|
+
.patch(
|
|
178
|
+
name=resource_name,
|
|
179
|
+
body=body,
|
|
180
|
+
updateMask=update_mask,
|
|
181
|
+
)
|
|
182
|
+
.execute()
|
|
183
|
+
)
|
|
184
|
+
output(group, effective_format)
|
|
185
|
+
except typer.BadParameter:
|
|
186
|
+
raise
|
|
187
|
+
except Exception as e:
|
|
188
|
+
handle_error(e)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@channel_groups_app.command("delete")
|
|
192
|
+
def delete_cmd(
|
|
193
|
+
property_id: Optional[str] = typer.Option(
|
|
194
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
195
|
+
),
|
|
196
|
+
channel_group_id: str = typer.Option(
|
|
197
|
+
..., "--channel-group-id", "-g", help="Channel group ID"
|
|
198
|
+
),
|
|
199
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
200
|
+
):
|
|
201
|
+
"""Delete a channel group."""
|
|
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"Delete channel group {channel_group_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}/channelGroups/{channel_group_id}"
|
|
216
|
+
admin.properties().channelGroups().delete(
|
|
217
|
+
name=resource_name
|
|
218
|
+
).execute()
|
|
219
|
+
success(f"Channel group {channel_group_id} deleted.")
|
|
220
|
+
except typer.Exit:
|
|
221
|
+
raise
|
|
222
|
+
except Exception as e:
|
|
223
|
+
handle_error(e)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Shell completion script generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from click.shell_completion import (
|
|
7
|
+
BashComplete,
|
|
8
|
+
FishComplete,
|
|
9
|
+
ZshComplete,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
completions_app = typer.Typer(
|
|
13
|
+
name="completions", help="Generate shell completion scripts", no_args_is_help=True
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# The prog_name used by the installed entry point
|
|
17
|
+
_PROG_NAME = "ga"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_source_vars() -> dict[str, str]:
|
|
21
|
+
"""Build the template vars that Click's completion classes need."""
|
|
22
|
+
func_name = f"_{_PROG_NAME}_completion"
|
|
23
|
+
complete_var = f"_{_PROG_NAME.upper()}_COMPLETE"
|
|
24
|
+
return {
|
|
25
|
+
"complete_func": func_name,
|
|
26
|
+
"complete_var": complete_var,
|
|
27
|
+
"prog_name": _PROG_NAME,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@completions_app.command("bash")
|
|
32
|
+
def bash_cmd():
|
|
33
|
+
"""Generate bash completion script.
|
|
34
|
+
|
|
35
|
+
Usage: ga completions bash > ~/.bash_completion.d/ga
|
|
36
|
+
"""
|
|
37
|
+
print(BashComplete.source_template % _get_source_vars())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@completions_app.command("zsh")
|
|
41
|
+
def zsh_cmd():
|
|
42
|
+
"""Generate zsh completion script.
|
|
43
|
+
|
|
44
|
+
Usage: ga completions zsh > ~/.zsh/completions/_ga
|
|
45
|
+
"""
|
|
46
|
+
print(ZshComplete.source_template % _get_source_vars())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@completions_app.command("fish")
|
|
50
|
+
def fish_cmd():
|
|
51
|
+
"""Generate fish completion script.
|
|
52
|
+
|
|
53
|
+
Usage: ga completions fish > ~/.config/fish/completions/ga.fish
|
|
54
|
+
"""
|
|
55
|
+
print(FishComplete.source_template % _get_source_vars())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Configuration management commands.
|
|
2
|
+
|
|
3
|
+
Equivalent to GTM CLI's commands/config.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ..config.store import (
|
|
11
|
+
VALID_CONFIG_KEYS,
|
|
12
|
+
UserConfig,
|
|
13
|
+
clear_config,
|
|
14
|
+
get_config_path,
|
|
15
|
+
get_config_value,
|
|
16
|
+
load_config,
|
|
17
|
+
save_config,
|
|
18
|
+
set_config_value,
|
|
19
|
+
unset_config_value,
|
|
20
|
+
)
|
|
21
|
+
from ..utils import error, info, output, success
|
|
22
|
+
|
|
23
|
+
config_app = typer.Typer(name="config", help="Manage CLI configuration", no_args_is_help=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@config_app.command("setup")
|
|
27
|
+
def setup():
|
|
28
|
+
"""Interactive configuration wizard."""
|
|
29
|
+
import questionary
|
|
30
|
+
|
|
31
|
+
info("GA CLI Configuration Setup")
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
account_id = questionary.text(
|
|
35
|
+
"Default Account ID (leave empty to skip):"
|
|
36
|
+
).ask()
|
|
37
|
+
|
|
38
|
+
property_id = questionary.text(
|
|
39
|
+
"Default Property ID (leave empty to skip):"
|
|
40
|
+
).ask()
|
|
41
|
+
|
|
42
|
+
output_format = questionary.select(
|
|
43
|
+
"Default output format:",
|
|
44
|
+
choices=["table", "json", "compact"],
|
|
45
|
+
default="table",
|
|
46
|
+
).ask()
|
|
47
|
+
|
|
48
|
+
config = UserConfig(
|
|
49
|
+
default_account_id=account_id or None,
|
|
50
|
+
default_property_id=property_id or None,
|
|
51
|
+
output_format=output_format or "table",
|
|
52
|
+
)
|
|
53
|
+
save_config(config)
|
|
54
|
+
success("Configuration saved.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@config_app.command("get")
|
|
58
|
+
def get_cmd(key: Optional[str] = typer.Argument(None, help="Config key to retrieve")):
|
|
59
|
+
"""Get configuration values."""
|
|
60
|
+
if key:
|
|
61
|
+
if key not in VALID_CONFIG_KEYS:
|
|
62
|
+
error(f"Unknown config key: {key}. Valid keys: {', '.join(VALID_CONFIG_KEYS)}")
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
value = get_config_value(key)
|
|
65
|
+
if value is not None:
|
|
66
|
+
print(value)
|
|
67
|
+
else:
|
|
68
|
+
error(f"Config key '{key}' is not set.")
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
else:
|
|
71
|
+
from dataclasses import asdict
|
|
72
|
+
config = load_config()
|
|
73
|
+
output(asdict(config), "json")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@config_app.command("set")
|
|
77
|
+
def set_cmd(
|
|
78
|
+
key: str = typer.Argument(help="Config key"),
|
|
79
|
+
value: str = typer.Argument(help="Config value"),
|
|
80
|
+
):
|
|
81
|
+
"""Set a configuration value."""
|
|
82
|
+
if key not in VALID_CONFIG_KEYS:
|
|
83
|
+
error(f"Unknown config key: {key}. Valid keys: {', '.join(VALID_CONFIG_KEYS)}")
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
set_config_value(key, value)
|
|
86
|
+
success(f"Set {key} = {value}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@config_app.command("unset")
|
|
90
|
+
def unset(key: str = typer.Argument(help="Config key to remove")):
|
|
91
|
+
"""Remove a configuration value."""
|
|
92
|
+
if key not in VALID_CONFIG_KEYS:
|
|
93
|
+
error(f"Unknown config key: {key}. Valid keys: {', '.join(VALID_CONFIG_KEYS)}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
unset_config_value(key)
|
|
96
|
+
success(f"Unset {key}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@config_app.command("path")
|
|
100
|
+
def path():
|
|
101
|
+
"""Show configuration file path."""
|
|
102
|
+
print(get_config_path())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@config_app.command("reset")
|
|
106
|
+
def reset():
|
|
107
|
+
"""Reset all configuration to defaults."""
|
|
108
|
+
import questionary
|
|
109
|
+
if questionary.confirm("Reset all configuration to defaults?", default=False).ask():
|
|
110
|
+
clear_config()
|
|
111
|
+
success("Configuration reset.")
|
|
112
|
+
else:
|
|
113
|
+
info("Reset cancelled.")
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Custom dimension 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
|
+
custom_dimensions_app = typer.Typer(
|
|
22
|
+
name="custom-dimensions",
|
|
23
|
+
help="Manage custom dimensions",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_VALID_SCOPES = ("EVENT", "USER", "ITEM")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@custom_dimensions_app.command("list")
|
|
31
|
+
def list_cmd(
|
|
32
|
+
property_id: Optional[str] = typer.Option(
|
|
33
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
34
|
+
),
|
|
35
|
+
output_format: Optional[str] = typer.Option(
|
|
36
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
37
|
+
),
|
|
38
|
+
):
|
|
39
|
+
"""List custom dimensions for a property."""
|
|
40
|
+
try:
|
|
41
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
42
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
43
|
+
effective_format = resolve_output_format(output_format)
|
|
44
|
+
|
|
45
|
+
admin = get_admin_client()
|
|
46
|
+
dimensions = paginate_all(
|
|
47
|
+
lambda **kw: admin.properties()
|
|
48
|
+
.customDimensions()
|
|
49
|
+
.list(parent=f"properties/{effective_property}", **kw)
|
|
50
|
+
.execute(),
|
|
51
|
+
"customDimensions",
|
|
52
|
+
pageSize=200,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
output(
|
|
56
|
+
dimensions,
|
|
57
|
+
effective_format,
|
|
58
|
+
columns=[
|
|
59
|
+
"name",
|
|
60
|
+
"parameterName",
|
|
61
|
+
"displayName",
|
|
62
|
+
"scope",
|
|
63
|
+
"description",
|
|
64
|
+
"disallowAdsPersonalization",
|
|
65
|
+
],
|
|
66
|
+
headers=[
|
|
67
|
+
"Resource Name",
|
|
68
|
+
"Parameter Name",
|
|
69
|
+
"Display Name",
|
|
70
|
+
"Scope",
|
|
71
|
+
"Description",
|
|
72
|
+
"Disallow Ads",
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
handle_error(e)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@custom_dimensions_app.command("get")
|
|
80
|
+
def get_cmd(
|
|
81
|
+
property_id: Optional[str] = typer.Option(
|
|
82
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
83
|
+
),
|
|
84
|
+
dimension_id: str = typer.Option(
|
|
85
|
+
..., "--dimension-id", "-d", help="Custom dimension ID"
|
|
86
|
+
),
|
|
87
|
+
output_format: Optional[str] = typer.Option(
|
|
88
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
89
|
+
),
|
|
90
|
+
):
|
|
91
|
+
"""Get details for a custom dimension."""
|
|
92
|
+
try:
|
|
93
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
94
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
95
|
+
effective_format = resolve_output_format(output_format)
|
|
96
|
+
|
|
97
|
+
admin = get_admin_client()
|
|
98
|
+
dimension = (
|
|
99
|
+
admin.properties()
|
|
100
|
+
.customDimensions()
|
|
101
|
+
.get(
|
|
102
|
+
name=f"properties/{effective_property}/customDimensions/{dimension_id}"
|
|
103
|
+
)
|
|
104
|
+
.execute()
|
|
105
|
+
)
|
|
106
|
+
output(dimension, effective_format)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
handle_error(e)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@custom_dimensions_app.command("create")
|
|
112
|
+
def create_cmd(
|
|
113
|
+
property_id: Optional[str] = typer.Option(
|
|
114
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
115
|
+
),
|
|
116
|
+
parameter_name: str = typer.Option(
|
|
117
|
+
..., "--parameter-name", help="Event parameter name"
|
|
118
|
+
),
|
|
119
|
+
display_name: str = typer.Option(..., "--display-name", help="Display name in GA4 UI"),
|
|
120
|
+
scope: str = typer.Option(..., "--scope", help="Scope: EVENT, USER, or ITEM"),
|
|
121
|
+
description: str = typer.Option("", "--description", help="Description"),
|
|
122
|
+
disallow_ads: bool = typer.Option(
|
|
123
|
+
False, "--disallow-ads", help="Disallow ads personalization"
|
|
124
|
+
),
|
|
125
|
+
dry_run: bool = typer.Option(
|
|
126
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
127
|
+
),
|
|
128
|
+
output_format: Optional[str] = typer.Option(
|
|
129
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
130
|
+
),
|
|
131
|
+
):
|
|
132
|
+
"""Create a custom dimension."""
|
|
133
|
+
try:
|
|
134
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
135
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
136
|
+
effective_format = resolve_output_format(output_format)
|
|
137
|
+
|
|
138
|
+
scope_upper = scope.upper()
|
|
139
|
+
if scope_upper not in _VALID_SCOPES:
|
|
140
|
+
raise typer.BadParameter(
|
|
141
|
+
f"Invalid scope '{scope}'. Must be one of: {', '.join(_VALID_SCOPES)}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
body = {
|
|
145
|
+
"parameterName": parameter_name,
|
|
146
|
+
"displayName": display_name,
|
|
147
|
+
"scope": scope_upper,
|
|
148
|
+
"description": description,
|
|
149
|
+
"disallowAdsPersonalization": disallow_ads,
|
|
150
|
+
}
|
|
151
|
+
if dry_run:
|
|
152
|
+
handle_dry_run("create", "POST", f"properties/{effective_property}", body)
|
|
153
|
+
|
|
154
|
+
admin = get_admin_client()
|
|
155
|
+
dimension = (
|
|
156
|
+
admin.properties()
|
|
157
|
+
.customDimensions()
|
|
158
|
+
.create(parent=f"properties/{effective_property}", body=body)
|
|
159
|
+
.execute()
|
|
160
|
+
)
|
|
161
|
+
output(dimension, effective_format)
|
|
162
|
+
except (typer.BadParameter, typer.Exit):
|
|
163
|
+
raise
|
|
164
|
+
except Exception as e:
|
|
165
|
+
handle_error(e)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@custom_dimensions_app.command("update")
|
|
169
|
+
def update_cmd(
|
|
170
|
+
property_id: Optional[str] = typer.Option(
|
|
171
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
172
|
+
),
|
|
173
|
+
dimension_id: str = typer.Option(
|
|
174
|
+
..., "--dimension-id", "-d", help="Custom dimension ID"
|
|
175
|
+
),
|
|
176
|
+
display_name: Optional[str] = typer.Option(None, "--display-name", help="New display name"),
|
|
177
|
+
description: Optional[str] = typer.Option(None, "--description", help="New description"),
|
|
178
|
+
dry_run: bool = typer.Option(
|
|
179
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
180
|
+
),
|
|
181
|
+
output_format: Optional[str] = typer.Option(
|
|
182
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
183
|
+
),
|
|
184
|
+
):
|
|
185
|
+
"""Update a custom dimension."""
|
|
186
|
+
try:
|
|
187
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
188
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
189
|
+
effective_format = resolve_output_format(output_format)
|
|
190
|
+
|
|
191
|
+
body = {}
|
|
192
|
+
mask_fields = []
|
|
193
|
+
if display_name is not None:
|
|
194
|
+
body["displayName"] = display_name
|
|
195
|
+
mask_fields.append("displayName")
|
|
196
|
+
if description is not None:
|
|
197
|
+
body["description"] = description
|
|
198
|
+
mask_fields.append("description")
|
|
199
|
+
|
|
200
|
+
if not mask_fields:
|
|
201
|
+
raise typer.BadParameter(
|
|
202
|
+
"At least one field must be specified: --display-name, --description"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
resource_name = f"properties/{effective_property}/customDimensions/{dimension_id}"
|
|
206
|
+
if dry_run:
|
|
207
|
+
handle_dry_run(
|
|
208
|
+
"update", "PATCH", resource_name,
|
|
209
|
+
body, update_mask=",".join(mask_fields),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
admin = get_admin_client()
|
|
213
|
+
dimension = (
|
|
214
|
+
admin.properties()
|
|
215
|
+
.customDimensions()
|
|
216
|
+
.patch(
|
|
217
|
+
name=resource_name,
|
|
218
|
+
body=body,
|
|
219
|
+
updateMask=",".join(mask_fields),
|
|
220
|
+
)
|
|
221
|
+
.execute()
|
|
222
|
+
)
|
|
223
|
+
output(dimension, effective_format)
|
|
224
|
+
except (typer.BadParameter, typer.Exit):
|
|
225
|
+
raise
|
|
226
|
+
except Exception as e:
|
|
227
|
+
handle_error(e)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@custom_dimensions_app.command("archive")
|
|
231
|
+
def archive_cmd(
|
|
232
|
+
property_id: Optional[str] = typer.Option(
|
|
233
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
234
|
+
),
|
|
235
|
+
dimension_id: str = typer.Option(
|
|
236
|
+
..., "--dimension-id", "-d", help="Custom dimension ID"
|
|
237
|
+
),
|
|
238
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
239
|
+
dry_run: bool = typer.Option(
|
|
240
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
241
|
+
),
|
|
242
|
+
):
|
|
243
|
+
"""Archive a custom dimension."""
|
|
244
|
+
try:
|
|
245
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
246
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
247
|
+
|
|
248
|
+
if dry_run:
|
|
249
|
+
handle_dry_run(
|
|
250
|
+
"archive", "POST",
|
|
251
|
+
f"properties/{effective_property}/customDimensions/{dimension_id}",
|
|
252
|
+
None,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if not yes:
|
|
256
|
+
confirmed = questionary.confirm(
|
|
257
|
+
f"Archive custom dimension {dimension_id}? This cannot be undone."
|
|
258
|
+
).ask()
|
|
259
|
+
if not confirmed:
|
|
260
|
+
info("Cancelled.")
|
|
261
|
+
raise typer.Exit()
|
|
262
|
+
|
|
263
|
+
admin = get_admin_client()
|
|
264
|
+
resource_name = f"properties/{effective_property}/customDimensions/{dimension_id}"
|
|
265
|
+
admin.properties().customDimensions().archive(
|
|
266
|
+
name=resource_name, body={}
|
|
267
|
+
).execute()
|
|
268
|
+
success(f"Custom dimension {dimension_id} archived.")
|
|
269
|
+
except typer.Exit:
|
|
270
|
+
raise
|
|
271
|
+
except Exception as e:
|
|
272
|
+
handle_error(e)
|