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.
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.0rc1.dist-info/METADATA +269 -0
  47. google_analytics_cli-0.1.0rc1.dist-info/RECORD +49 -0
  48. google_analytics_cli-0.1.0rc1.dist-info/WHEEL +4 -0
  49. 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)