ga4-cli 0.1.4__tar.gz → 0.2.0__tar.gz

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 (41) hide show
  1. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/PKG-INFO +27 -16
  2. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/README.md +21 -11
  3. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/PKG-INFO +27 -16
  4. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/SOURCES.txt +3 -0
  5. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/requires.txt +1 -1
  6. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/__init__.py +1 -1
  7. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/config.py +37 -8
  8. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/datastreams.py +1 -5
  9. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/properties.py +19 -6
  10. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/decorators.py +2 -3
  11. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/output.py +3 -2
  12. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/setup.py +5 -4
  13. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/conftest.py +22 -5
  14. ga4_cli-0.2.0/tests/test_accounts.py +71 -0
  15. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_auth.py +1 -2
  16. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_cli.py +2 -1
  17. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_config.py +0 -2
  18. ga4_cli-0.2.0/tests/test_config_commands.py +71 -0
  19. ga4_cli-0.2.0/tests/test_datastreams.py +86 -0
  20. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_errors.py +0 -2
  21. ga4_cli-0.2.0/tests/test_properties.py +134 -0
  22. ga4_cli-0.1.4/tests/test_accounts.py +0 -49
  23. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/LICENSE +0 -0
  24. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/dependency_links.txt +0 -0
  25. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/entry_points.txt +0 -0
  26. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/top_level.txt +0 -0
  27. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/auth.py +0 -0
  28. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/cli.py +0 -0
  29. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/__init__.py +0 -0
  30. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/accounts.py +0 -0
  31. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/config.py +0 -0
  32. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/errors.py +0 -0
  33. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/__init__.py +0 -0
  34. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/json.py +0 -0
  35. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/table.py +0 -0
  36. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/logging_config.py +0 -0
  37. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/retry.py +0 -0
  38. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/validators.py +0 -0
  39. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/setup.cfg +0 -0
  40. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/__init__.py +0 -0
  41. {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ga4-cli
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: Command-line interface for Google Analytics 4
5
5
  Home-page: https://github.com/sulimanbenhalim/ga-cli
6
6
  Author: Suliman Ben Halim
@@ -15,17 +15,18 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
18
  Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
22
- Requires-Python: >=3.8
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Requires-Python: >=3.10
23
24
  Description-Content-Type: text/markdown
24
25
  License-File: LICENSE
25
26
  Requires-Dist: click<9.0.0,>=8.0.0
26
27
  Requires-Dist: google-analytics-admin<1.0.0,>=0.27.0
27
28
  Requires-Dist: google-auth<3.0.0,>=2.0.0
28
- Requires-Dist: rich<14.0.0,>=13.0.0
29
+ Requires-Dist: rich<16.0.0,>=13.0.0
29
30
  Requires-Dist: pytz>=2023.3
30
31
  Dynamic: author
31
32
  Dynamic: author-email
@@ -71,13 +72,15 @@ ga-cli --version
71
72
  ga-cli --help
72
73
  ```
73
74
 
74
- ### Using Docker
75
+ ### Using pipx (recommended for a global command)
75
76
 
76
77
  ```bash
77
- # Pull from Docker Hub
78
- docker pull sulimanbenhalim/ga-cli:latest
78
+ pipx install ga4-cli
79
+ ```
80
+
81
+ ### Using Docker (build locally)
79
82
 
80
- # Or build locally
83
+ ```bash
81
84
  docker build -t ga-cli .
82
85
 
83
86
  # Run with credentials mounted
@@ -90,10 +93,17 @@ docker run -v /path/to/credentials.json:/credentials.json \
90
93
 
91
94
  Download standalone binaries from [GitHub Releases](https://github.com/sulimanbenhalim/ga-cli/releases):
92
95
 
96
+ | File | Platform |
97
+ |---|---|
98
+ | `ga-cli-linux-amd64.tar.gz` | Linux x86_64 |
99
+ | `ga-cli-macos-arm64.tar.gz` | macOS Apple Silicon |
100
+ | `ga-cli-macos-amd64.tar.gz` | macOS Intel |
101
+ | `ga-cli-windows-amd64.exe.zip` | Windows x86_64 |
102
+
93
103
  **Linux/macOS:**
94
104
  ```bash
95
- # Download and extract
96
- curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-linux-amd64.tar.gz
105
+ # Pick the file for your platform from the table above
106
+ curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-macos-arm64.tar.gz
97
107
  tar -xzf ga-cli.tar.gz
98
108
  chmod +x ga-cli
99
109
  sudo mv ga-cli /usr/local/bin/
@@ -117,8 +127,9 @@ pip install -e .
117
127
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
118
128
  2. Create or select a project
119
129
  3. Enable the Google Analytics Admin API
120
- 4. Create a service account with Analytics Admin permissions
130
+ 4. Create a service account
121
131
  5. Download the JSON credentials file
132
+ 6. In Google Analytics, go to **Admin > Account access management**, click **+**, and add the service account's email (the `client_email` value in the JSON file) with the **Editor** role
122
133
 
123
134
  ### 2. Initialize GA CLI
124
135
 
@@ -126,7 +137,7 @@ pip install -e .
126
137
  ga-cli config init
127
138
  ```
128
139
 
129
- This will prompt you for the path to your service account JSON file and test the credentials.
140
+ This will prompt you for the path to your service account JSON file, test the credentials, and save the path. If you have not done step 6 yet, it prints the exact email to add and you can carry on afterwards without running `init` again.
130
141
 
131
142
  Alternatively, set the environment variable:
132
143
 
@@ -171,7 +182,7 @@ ga-cli properties get <property-id>
171
182
  # Create a new property
172
183
  ga-cli properties create <account-id> --name "My Website" --timezone "America/New_York" --currency "USD"
173
184
 
174
- # Delete a property
185
+ # Move a property to the trash (restorable from the GA Admin UI until purged)
175
186
  ga-cli properties delete <property-id>
176
187
  ```
177
188
 
@@ -275,11 +286,11 @@ ga-cli/
275
286
 
276
287
  ## Requirements
277
288
 
278
- - Python 3.8+
289
+ - Python 3.10+
279
290
  - Click 8.0+
280
291
  - google-analytics-admin 0.27.0+
281
292
  - google-auth 2.0+
282
- - rich 13.0+
293
+ - rich 13.0+ (up to 15.x)
283
294
  - pytz 2023.3+
284
295
 
285
296
  ## License
@@ -28,13 +28,15 @@ ga-cli --version
28
28
  ga-cli --help
29
29
  ```
30
30
 
31
- ### Using Docker
31
+ ### Using pipx (recommended for a global command)
32
32
 
33
33
  ```bash
34
- # Pull from Docker Hub
35
- docker pull sulimanbenhalim/ga-cli:latest
34
+ pipx install ga4-cli
35
+ ```
36
+
37
+ ### Using Docker (build locally)
36
38
 
37
- # Or build locally
39
+ ```bash
38
40
  docker build -t ga-cli .
39
41
 
40
42
  # Run with credentials mounted
@@ -47,10 +49,17 @@ docker run -v /path/to/credentials.json:/credentials.json \
47
49
 
48
50
  Download standalone binaries from [GitHub Releases](https://github.com/sulimanbenhalim/ga-cli/releases):
49
51
 
52
+ | File | Platform |
53
+ |---|---|
54
+ | `ga-cli-linux-amd64.tar.gz` | Linux x86_64 |
55
+ | `ga-cli-macos-arm64.tar.gz` | macOS Apple Silicon |
56
+ | `ga-cli-macos-amd64.tar.gz` | macOS Intel |
57
+ | `ga-cli-windows-amd64.exe.zip` | Windows x86_64 |
58
+
50
59
  **Linux/macOS:**
51
60
  ```bash
52
- # Download and extract
53
- curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-linux-amd64.tar.gz
61
+ # Pick the file for your platform from the table above
62
+ curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-macos-arm64.tar.gz
54
63
  tar -xzf ga-cli.tar.gz
55
64
  chmod +x ga-cli
56
65
  sudo mv ga-cli /usr/local/bin/
@@ -74,8 +83,9 @@ pip install -e .
74
83
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
75
84
  2. Create or select a project
76
85
  3. Enable the Google Analytics Admin API
77
- 4. Create a service account with Analytics Admin permissions
86
+ 4. Create a service account
78
87
  5. Download the JSON credentials file
88
+ 6. In Google Analytics, go to **Admin > Account access management**, click **+**, and add the service account's email (the `client_email` value in the JSON file) with the **Editor** role
79
89
 
80
90
  ### 2. Initialize GA CLI
81
91
 
@@ -83,7 +93,7 @@ pip install -e .
83
93
  ga-cli config init
84
94
  ```
85
95
 
86
- This will prompt you for the path to your service account JSON file and test the credentials.
96
+ This will prompt you for the path to your service account JSON file, test the credentials, and save the path. If you have not done step 6 yet, it prints the exact email to add and you can carry on afterwards without running `init` again.
87
97
 
88
98
  Alternatively, set the environment variable:
89
99
 
@@ -128,7 +138,7 @@ ga-cli properties get <property-id>
128
138
  # Create a new property
129
139
  ga-cli properties create <account-id> --name "My Website" --timezone "America/New_York" --currency "USD"
130
140
 
131
- # Delete a property
141
+ # Move a property to the trash (restorable from the GA Admin UI until purged)
132
142
  ga-cli properties delete <property-id>
133
143
  ```
134
144
 
@@ -232,11 +242,11 @@ ga-cli/
232
242
 
233
243
  ## Requirements
234
244
 
235
- - Python 3.8+
245
+ - Python 3.10+
236
246
  - Click 8.0+
237
247
  - google-analytics-admin 0.27.0+
238
248
  - google-auth 2.0+
239
- - rich 13.0+
249
+ - rich 13.0+ (up to 15.x)
240
250
  - pytz 2023.3+
241
251
 
242
252
  ## License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ga4-cli
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: Command-line interface for Google Analytics 4
5
5
  Home-page: https://github.com/sulimanbenhalim/ga-cli
6
6
  Author: Suliman Ben Halim
@@ -15,17 +15,18 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
18
  Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
22
- Requires-Python: >=3.8
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Requires-Python: >=3.10
23
24
  Description-Content-Type: text/markdown
24
25
  License-File: LICENSE
25
26
  Requires-Dist: click<9.0.0,>=8.0.0
26
27
  Requires-Dist: google-analytics-admin<1.0.0,>=0.27.0
27
28
  Requires-Dist: google-auth<3.0.0,>=2.0.0
28
- Requires-Dist: rich<14.0.0,>=13.0.0
29
+ Requires-Dist: rich<16.0.0,>=13.0.0
29
30
  Requires-Dist: pytz>=2023.3
30
31
  Dynamic: author
31
32
  Dynamic: author-email
@@ -71,13 +72,15 @@ ga-cli --version
71
72
  ga-cli --help
72
73
  ```
73
74
 
74
- ### Using Docker
75
+ ### Using pipx (recommended for a global command)
75
76
 
76
77
  ```bash
77
- # Pull from Docker Hub
78
- docker pull sulimanbenhalim/ga-cli:latest
78
+ pipx install ga4-cli
79
+ ```
80
+
81
+ ### Using Docker (build locally)
79
82
 
80
- # Or build locally
83
+ ```bash
81
84
  docker build -t ga-cli .
82
85
 
83
86
  # Run with credentials mounted
@@ -90,10 +93,17 @@ docker run -v /path/to/credentials.json:/credentials.json \
90
93
 
91
94
  Download standalone binaries from [GitHub Releases](https://github.com/sulimanbenhalim/ga-cli/releases):
92
95
 
96
+ | File | Platform |
97
+ |---|---|
98
+ | `ga-cli-linux-amd64.tar.gz` | Linux x86_64 |
99
+ | `ga-cli-macos-arm64.tar.gz` | macOS Apple Silicon |
100
+ | `ga-cli-macos-amd64.tar.gz` | macOS Intel |
101
+ | `ga-cli-windows-amd64.exe.zip` | Windows x86_64 |
102
+
93
103
  **Linux/macOS:**
94
104
  ```bash
95
- # Download and extract
96
- curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-linux-amd64.tar.gz
105
+ # Pick the file for your platform from the table above
106
+ curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-macos-arm64.tar.gz
97
107
  tar -xzf ga-cli.tar.gz
98
108
  chmod +x ga-cli
99
109
  sudo mv ga-cli /usr/local/bin/
@@ -117,8 +127,9 @@ pip install -e .
117
127
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
118
128
  2. Create or select a project
119
129
  3. Enable the Google Analytics Admin API
120
- 4. Create a service account with Analytics Admin permissions
130
+ 4. Create a service account
121
131
  5. Download the JSON credentials file
132
+ 6. In Google Analytics, go to **Admin > Account access management**, click **+**, and add the service account's email (the `client_email` value in the JSON file) with the **Editor** role
122
133
 
123
134
  ### 2. Initialize GA CLI
124
135
 
@@ -126,7 +137,7 @@ pip install -e .
126
137
  ga-cli config init
127
138
  ```
128
139
 
129
- This will prompt you for the path to your service account JSON file and test the credentials.
140
+ This will prompt you for the path to your service account JSON file, test the credentials, and save the path. If you have not done step 6 yet, it prints the exact email to add and you can carry on afterwards without running `init` again.
130
141
 
131
142
  Alternatively, set the environment variable:
132
143
 
@@ -171,7 +182,7 @@ ga-cli properties get <property-id>
171
182
  # Create a new property
172
183
  ga-cli properties create <account-id> --name "My Website" --timezone "America/New_York" --currency "USD"
173
184
 
174
- # Delete a property
185
+ # Move a property to the trash (restorable from the GA Admin UI until purged)
175
186
  ga-cli properties delete <property-id>
176
187
  ```
177
188
 
@@ -275,11 +286,11 @@ ga-cli/
275
286
 
276
287
  ## Requirements
277
288
 
278
- - Python 3.8+
289
+ - Python 3.10+
279
290
  - Click 8.0+
280
291
  - google-analytics-admin 0.27.0+
281
292
  - google-auth 2.0+
282
- - rich 13.0+
293
+ - rich 13.0+ (up to 15.x)
283
294
  - pytz 2023.3+
284
295
 
285
296
  ## License
@@ -31,5 +31,8 @@ tests/test_accounts.py
31
31
  tests/test_auth.py
32
32
  tests/test_cli.py
33
33
  tests/test_config.py
34
+ tests/test_config_commands.py
35
+ tests/test_datastreams.py
34
36
  tests/test_errors.py
37
+ tests/test_properties.py
35
38
  tests/test_validators.py
@@ -1,5 +1,5 @@
1
1
  click<9.0.0,>=8.0.0
2
2
  google-analytics-admin<1.0.0,>=0.27.0
3
3
  google-auth<3.0.0,>=2.0.0
4
- rich<14.0.0,>=13.0.0
4
+ rich<16.0.0,>=13.0.0
5
5
  pytz>=2023.3
@@ -1,3 +1,3 @@
1
1
  """Google Analytics CLI - Manage GA4 from the command line"""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.2.0"
@@ -1,9 +1,12 @@
1
1
  """Configuration commands"""
2
2
 
3
+ import json
3
4
  import click
4
5
  import os
6
+ from google.api_core import exceptions
5
7
  from ga_cli.config import ConfigManager
6
8
  from ga_cli.auth import AuthManager
9
+ from ga_cli.errors import get_friendly_error
7
10
 
8
11
 
9
12
  @click.group()
@@ -12,6 +15,15 @@ def config():
12
15
  pass
13
16
 
14
17
 
18
+ def _read_client_email(credentials_path):
19
+ """Return the service account email from the JSON key, or None if unreadable"""
20
+ try:
21
+ with open(credentials_path, 'r', encoding='utf-8') as f:
22
+ return json.load(f).get('client_email')
23
+ except (OSError, ValueError, AttributeError):
24
+ return None
25
+
26
+
15
27
  @config.command()
16
28
  @click.option('--credentials', prompt='Path to service account JSON',
17
29
  help='Path to Google service account credentials file')
@@ -24,23 +36,37 @@ def init(credentials):
24
36
  click.echo(f"Error: Credentials file not found at {credentials_path}", err=True)
25
37
  raise click.Abort()
26
38
 
39
+ client_email = _read_client_email(credentials_path)
40
+
27
41
  click.echo("Testing credentials...")
28
42
  auth = AuthManager(credentials_path)
29
43
  client = auth.get_client()
30
-
31
44
  accounts = list(client.list_accounts())
32
45
 
46
+ # The key file is valid at this point, so save it even if the service
47
+ # account has not been granted access to any GA account yet. Otherwise
48
+ # the user has to come back and run init a second time.
49
+ config_manager = ConfigManager()
50
+ config_manager.set_credentials_path(credentials_path)
51
+ click.echo(f"Configuration saved to {config_manager.config_file}")
52
+
33
53
  if accounts:
34
54
  click.echo(f"Credentials valid! Found {len(accounts)} account(s)")
35
-
36
- config_manager = ConfigManager()
37
- config_manager.set_credentials_path(credentials_path)
38
-
39
- click.echo(f"Configuration saved to {config_manager.config_file}")
40
55
  click.echo("\nYou can now use ga-cli commands without specifying credentials")
41
56
  else:
42
- click.echo("Warning: Credentials are valid but no accounts found", err=True)
43
-
57
+ click.echo("\nCredentials are valid, but this service account cannot see any "
58
+ "Google Analytics accounts yet.")
59
+ if client_email:
60
+ click.echo(f"\nAdd this email as a user in Google Analytics:\n {client_email}")
61
+ click.echo("\n Google Analytics > Admin > Account access management > + > Add users")
62
+ click.echo(" Use the Editor role to create properties and data streams.")
63
+ click.echo("\nThen run: ga-cli accounts list")
64
+
65
+ except click.Abort:
66
+ raise
67
+ except exceptions.GoogleAPIError as e:
68
+ click.echo(get_friendly_error(e), err=True)
69
+ raise click.Abort()
44
70
  except Exception as e:
45
71
  click.echo(f"Error: {str(e)}", err=True)
46
72
  raise click.Abort()
@@ -56,6 +82,9 @@ def show():
56
82
  if credentials_path:
57
83
  click.echo("Configuration:")
58
84
  click.echo(f" Credentials: {credentials_path}")
85
+ client_email = _read_client_email(credentials_path)
86
+ if client_email:
87
+ click.echo(f" Service account: {client_email}")
59
88
  click.echo(f" Config file: {config_manager.config_file}")
60
89
  else:
61
90
  click.echo("No configuration found. Run 'ga-cli config init' to set up.")
@@ -134,13 +134,9 @@ def _create_datastream_with_retry(client, property_id, name, url):
134
134
  data_stream = DataStream(
135
135
  display_name=name,
136
136
  type_=DataStream.DataStreamType.WEB_DATA_STREAM,
137
+ web_stream_data=DataStream.WebStreamData(default_uri=url),
137
138
  )
138
139
 
139
- # Safely initialize web_stream_data if not exists
140
- if data_stream.web_stream_data is None:
141
- data_stream.web_stream_data = DataStream.WebStreamData()
142
- data_stream.web_stream_data.default_uri = url
143
-
144
140
  return client.create_data_stream(
145
141
  parent=f"properties/{property_id}",
146
142
  data_stream=data_stream
@@ -2,7 +2,7 @@
2
2
 
3
3
  import builtins
4
4
  import click
5
- from google.analytics.admin_v1alpha.types import Property
5
+ from google.analytics.admin_v1alpha.types import IndustryCategory, Property
6
6
  from ga_cli.decorators import with_client
7
7
  from ga_cli.formatters.table import format_table
8
8
  from ga_cli.formatters.json import format_json
@@ -10,6 +10,13 @@ from ga_cli.validators import validate_account_id, validate_property_id, validat
10
10
  from ga_cli.logging_config import logger
11
11
  from ga_cli.retry import retry_on_transient_error
12
12
 
13
+ # Valid --industry values, taken from the API enum so they cannot drift.
14
+ # Shown lowercase for readability; matched case-insensitively.
15
+ INDUSTRY_CHOICES = sorted(
16
+ name.lower() for name in IndustryCategory.__members__
17
+ if name != 'INDUSTRY_CATEGORY_UNSPECIFIED'
18
+ )
19
+
13
20
 
14
21
  @click.group()
15
22
  def properties():
@@ -83,7 +90,9 @@ def get(ctx, property_id, format):
83
90
  help='Property timezone')
84
91
  @click.option('--currency', default='USD', callback=validate_currency,
85
92
  help='Property currency code (e.g., USD, EUR)')
86
- @click.option('--industry', default='OTHER', help='Industry category')
93
+ @click.option('--industry', default='other',
94
+ type=click.Choice(INDUSTRY_CHOICES, case_sensitive=False),
95
+ help='Industry category')
87
96
  @click.pass_context
88
97
  @with_client
89
98
  def create(ctx, account_id, name, timezone, currency, industry):
@@ -106,18 +115,20 @@ def create(ctx, account_id, name, timezone, currency, industry):
106
115
 
107
116
  @properties.command()
108
117
  @click.argument('property_id', callback=validate_property_id)
109
- @click.confirmation_option(prompt='Are you sure you want to delete this property?')
118
+ @click.confirmation_option(prompt='Move this property to the trash?')
110
119
  @click.pass_context
111
120
  @with_client
112
121
  def delete(ctx, property_id):
113
- """Delete a property"""
122
+ """Move a property to the trash (soft delete)"""
114
123
  client = ctx.obj['client']
115
124
  logger.info(f"Deleting property: {property_id}")
116
125
 
117
126
  _delete_property_with_retry(client, property_id)
118
127
 
119
128
  logger.info(f"Deleted property: {property_id}")
120
- click.echo(f"Property {property_id} deleted successfully")
129
+ click.echo(f"Property {property_id} moved to the trash.")
130
+ click.echo("It can be restored from the Google Analytics Admin UI "
131
+ "until it is permanently purged.")
121
132
 
122
133
 
123
134
  @retry_on_transient_error()
@@ -141,7 +152,9 @@ def _create_property_with_retry(client, account_id, name, timezone, currency, in
141
152
  display_name=name,
142
153
  time_zone=timezone,
143
154
  currency_code=currency,
144
- industry_category=industry,
155
+ # __members__ lookup rather than IndustryCategory[...] because
156
+ # proto-plus ships no type hints and mypy reads [] as a generic.
157
+ industry_category=IndustryCategory.__members__[industry.upper()],
145
158
  )
146
159
  )
147
160
 
@@ -3,6 +3,7 @@
3
3
  import functools
4
4
  import click
5
5
  from ga_cli.auth import AuthManager
6
+ from ga_cli.config import ConfigManager
6
7
  from ga_cli.logging_config import logger
7
8
  from ga_cli.errors import get_friendly_error
8
9
  from google.api_core import exceptions
@@ -32,9 +33,7 @@ def with_client(func):
32
33
  # Get credentials path
33
34
  credentials_path = ctx.obj.get('credentials')
34
35
  if not credentials_path:
35
- from ga_cli.config import ConfigManager
36
- config_manager = ConfigManager()
37
- credentials_path = config_manager.get('credentials', 'path')
36
+ credentials_path = ConfigManager().get_credentials_path()
38
37
 
39
38
  if not credentials_path:
40
39
  logger.error("No credentials configured")
@@ -5,6 +5,7 @@ import json
5
5
  from rich.console import Console
6
6
 
7
7
  console = Console()
8
+ err_console = Console(stderr=True)
8
9
 
9
10
 
10
11
  def success_message(message: str, format_type: str = 'text'):
@@ -28,9 +29,9 @@ def error_message(message: str, format_type: str = 'text'):
28
29
  format_type: Output format ('text' or 'json')
29
30
  """
30
31
  if format_type == 'json':
31
- click.echo(json.dumps({"success": False, "error": message}))
32
+ click.echo(json.dumps({"success": False, "error": message}), err=True)
32
33
  else:
33
- console.print(f"[red]{message}[/red]", err=True)
34
+ err_console.print(f"[red]{message}[/red]")
34
35
 
35
36
 
36
37
  def info_message(message: str, format_type: str = 'text'):
@@ -27,18 +27,19 @@ setup(
27
27
  "Topic :: Software Development :: Libraries :: Python Modules",
28
28
  "License :: OSI Approved :: MIT License",
29
29
  "Programming Language :: Python :: 3",
30
- "Programming Language :: Python :: 3.8",
31
- "Programming Language :: Python :: 3.9",
32
30
  "Programming Language :: Python :: 3.10",
33
31
  "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
34
35
  ],
35
36
  keywords="google-analytics ga4 cli command-line",
36
- python_requires=">=3.8",
37
+ python_requires=">=3.10",
37
38
  install_requires=[
38
39
  "click>=8.0.0,<9.0.0",
39
40
  "google-analytics-admin>=0.27.0,<1.0.0",
40
41
  "google-auth>=2.0.0,<3.0.0",
41
- "rich>=13.0.0,<14.0.0",
42
+ "rich>=13.0.0,<16.0.0",
42
43
  "pytz>=2023.3",
43
44
  ],
44
45
  entry_points={
@@ -1,14 +1,31 @@
1
1
  """Pytest fixtures and configuration"""
2
2
 
3
- import pytest
4
- from unittest.mock import Mock, MagicMock
5
- from click.testing import CliRunner
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+
7
+ # Point HOME at a throwaway directory BEFORE ga_cli is imported anywhere.
8
+ # ga_cli.logging_config creates ~/.ga-cli/ga-cli.log at import time, and
9
+ # ConfigManager reads ~/.ga-cli/config.ini, so without this the suite would
10
+ # write into (and read from) the developer's real configuration.
11
+ _TEST_HOME = tempfile.mkdtemp(prefix="ga-cli-test-home-")
12
+ os.environ["HOME"] = _TEST_HOME
13
+ os.environ["USERPROFILE"] = _TEST_HOME
14
+ os.environ.pop("GOOGLE_APPLICATION_CREDENTIALS", None)
15
+
16
+ import pytest # noqa: E402
17
+ from unittest.mock import Mock, MagicMock # noqa: E402
18
+ from click.testing import CliRunner # noqa: E402
19
+
20
+
21
+ def pytest_sessionfinish(session, exitstatus):
22
+ shutil.rmtree(_TEST_HOME, ignore_errors=True)
6
23
 
7
24
 
8
25
  @pytest.fixture
9
26
  def cli_runner():
10
- """Provide a Click CLI test runner"""
11
- return CliRunner()
27
+ """Provide a Click CLI test runner with a wide terminal so Rich never wraps"""
28
+ return CliRunner(env={"COLUMNS": "200"})
12
29
 
13
30
 
14
31
  @pytest.fixture
@@ -0,0 +1,71 @@
1
+ """Tests for account commands"""
2
+
3
+ import json
4
+ from unittest.mock import Mock, patch
5
+ from ga_cli.cli import cli
6
+
7
+
8
+ @patch('ga_cli.decorators.AuthManager')
9
+ def test_accounts_list(mock_auth, cli_runner, mock_account, mock_credentials_path):
10
+ """accounts list renders every account returned by the API"""
11
+ mock_client = Mock()
12
+ mock_client.list_accounts.return_value = [mock_account]
13
+ mock_auth.return_value.get_client.return_value = mock_client
14
+
15
+ result = cli_runner.invoke(cli, ['--credentials', mock_credentials_path, 'accounts', 'list'])
16
+
17
+ assert result.exit_code == 0, result.output
18
+ assert 'Test Account' in result.output
19
+ assert '123456' in result.output
20
+ mock_auth.assert_called_once_with(mock_credentials_path)
21
+
22
+
23
+ @patch('ga_cli.decorators.AuthManager')
24
+ def test_accounts_list_json(mock_auth, cli_runner, mock_account, mock_credentials_path):
25
+ """accounts list --format json emits parseable JSON with the account fields"""
26
+ mock_client = Mock()
27
+ mock_client.list_accounts.return_value = [mock_account]
28
+ mock_auth.return_value.get_client.return_value = mock_client
29
+
30
+ result = cli_runner.invoke(
31
+ cli, ['--credentials', mock_credentials_path, 'accounts', 'list', '--format', 'json']
32
+ )
33
+
34
+ assert result.exit_code == 0, result.output
35
+ data = json.loads(result.output)
36
+ assert data == [{
37
+ 'id': '123456',
38
+ 'name': 'Test Account',
39
+ 'region': 'US',
40
+ 'create_time': '2023-01-01 00:00:00',
41
+ }]
42
+
43
+
44
+ @patch('ga_cli.decorators.AuthManager')
45
+ def test_accounts_get(mock_auth, cli_runner, mock_account, mock_credentials_path):
46
+ """accounts get fetches accounts/<id> and renders it"""
47
+ mock_client = Mock()
48
+ mock_client.get_account.return_value = mock_account
49
+ mock_auth.return_value.get_client.return_value = mock_client
50
+
51
+ result = cli_runner.invoke(cli, ['--credentials', mock_credentials_path, 'accounts', 'get', '123456'])
52
+
53
+ assert result.exit_code == 0, result.output
54
+ assert 'Test Account' in result.output
55
+ mock_client.get_account.assert_called_once_with(name='accounts/123456')
56
+
57
+
58
+ def test_accounts_get_rejects_non_numeric_id(cli_runner, mock_credentials_path):
59
+ """Validation fails before any API client is built"""
60
+ result = cli_runner.invoke(cli, ['--credentials', mock_credentials_path, 'accounts', 'get', 'abc'])
61
+
62
+ assert result.exit_code == 2
63
+ assert 'Account ID must be numeric' in result.output
64
+
65
+
66
+ def test_commands_fail_cleanly_without_credentials(cli_runner):
67
+ """No flag, no env var, no config file: a friendly error, not a traceback"""
68
+ result = cli_runner.invoke(cli, ['accounts', 'list'])
69
+
70
+ assert result.exit_code == 1
71
+ assert "Run 'ga-cli config init' first" in result.output
@@ -1,7 +1,6 @@
1
1
  """Tests for authentication module"""
2
2
 
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
3
+ from unittest.mock import Mock, patch
5
4
  from ga_cli.auth import AuthManager
6
5
 
7
6
 
@@ -1,6 +1,7 @@
1
1
  """Tests for CLI entry point"""
2
2
 
3
3
  from click.testing import CliRunner
4
+ from ga_cli import __version__
4
5
  from ga_cli.cli import cli
5
6
 
6
7
 
@@ -17,7 +18,7 @@ def test_cli_version():
17
18
  runner = CliRunner()
18
19
  result = runner.invoke(cli, ['--version'])
19
20
  assert result.exit_code == 0
20
- assert '0.1.0' in result.output
21
+ assert __version__ in result.output
21
22
 
22
23
 
23
24
  def test_accounts_command_exists():
@@ -2,9 +2,7 @@
2
2
 
3
3
  import pytest
4
4
  import os
5
- import stat
6
5
  from pathlib import Path
7
- from unittest.mock import Mock, patch
8
6
  import tempfile
9
7
  import click
10
8
  from ga_cli.config import ConfigManager
@@ -0,0 +1,71 @@
1
+ """Tests for the config command group (init / show) end to end"""
2
+
3
+ import json
4
+ from unittest.mock import patch
5
+ import pytest
6
+ from ga_cli.cli import cli
7
+ from ga_cli.config import ConfigManager
8
+
9
+
10
+ @pytest.fixture
11
+ def sa_key(tmp_path):
12
+ """A service-account JSON key with the fields config init reads"""
13
+ key = tmp_path / 'sa.json'
14
+ key.write_text(json.dumps({
15
+ 'type': 'service_account',
16
+ 'client_email': 'ga-bot@my-project.iam.gserviceaccount.com',
17
+ }))
18
+ return str(key)
19
+
20
+
21
+ @patch('ga_cli.commands.config.AuthManager')
22
+ def test_config_init_saves_and_reports_accounts(mock_auth, cli_runner, sa_key, mock_account):
23
+ mock_auth.return_value.get_client.return_value.list_accounts.return_value = [mock_account]
24
+
25
+ result = cli_runner.invoke(cli, ['config', 'init', '--credentials', sa_key])
26
+
27
+ assert result.exit_code == 0, result.output
28
+ assert 'Found 1 account(s)' in result.output
29
+ assert ConfigManager().get_credentials_path() == sa_key
30
+
31
+
32
+ @patch('ga_cli.commands.config.AuthManager')
33
+ def test_config_init_saves_even_with_no_accounts_and_prints_email(mock_auth, cli_runner, sa_key):
34
+ """First-time setup: the key is valid but GA access has not been granted yet"""
35
+ mock_auth.return_value.get_client.return_value.list_accounts.return_value = []
36
+
37
+ result = cli_runner.invoke(cli, ['config', 'init', '--credentials', sa_key])
38
+
39
+ assert result.exit_code == 0, result.output
40
+ assert 'ga-bot@my-project.iam.gserviceaccount.com' in result.output
41
+ assert 'Account access management' in result.output
42
+ # Config is saved so the user does not have to run init again after granting access
43
+ assert ConfigManager().get_credentials_path() == sa_key
44
+
45
+
46
+ def test_config_init_missing_file(cli_runner):
47
+ result = cli_runner.invoke(cli, ['config', 'init', '--credentials', '/nope/sa.json'])
48
+
49
+ assert result.exit_code == 1
50
+ assert 'Credentials file not found' in result.output
51
+
52
+
53
+ @patch('ga_cli.commands.config.AuthManager')
54
+ def test_config_show_after_init(mock_auth, cli_runner, sa_key, mock_account):
55
+ mock_auth.return_value.get_client.return_value.list_accounts.return_value = [mock_account]
56
+ cli_runner.invoke(cli, ['config', 'init', '--credentials', sa_key])
57
+
58
+ result = cli_runner.invoke(cli, ['config', 'show'])
59
+
60
+ assert result.exit_code == 0, result.output
61
+ assert sa_key in result.output
62
+ assert 'ga-bot@my-project.iam.gserviceaccount.com' in result.output
63
+
64
+
65
+ def test_config_show_without_config(cli_runner, tmp_path, monkeypatch):
66
+ monkeypatch.setenv('HOME', str(tmp_path))
67
+
68
+ result = cli_runner.invoke(cli, ['config', 'show'])
69
+
70
+ assert result.exit_code == 0, result.output
71
+ assert "Run 'ga-cli config init'" in result.output
@@ -0,0 +1,86 @@
1
+ """Tests for data stream commands"""
2
+
3
+ import json
4
+ from unittest.mock import Mock, patch
5
+ from google.analytics.admin_v1alpha.types import DataStream
6
+ from ga_cli.cli import cli
7
+
8
+
9
+ @patch('ga_cli.decorators.AuthManager')
10
+ def test_datastreams_list(mock_auth, cli_runner, mock_datastream, mock_credentials_path):
11
+ mock_client = Mock()
12
+ mock_client.list_data_streams.return_value = [mock_datastream]
13
+ mock_auth.return_value.get_client.return_value = mock_client
14
+
15
+ result = cli_runner.invoke(
16
+ cli, ['--credentials', mock_credentials_path, 'datastreams', 'list', '987654', '--format', 'json']
17
+ )
18
+
19
+ assert result.exit_code == 0, result.output
20
+ mock_client.list_data_streams.assert_called_once_with(parent='properties/987654')
21
+ data = json.loads(result.output)
22
+ assert data == [{
23
+ 'id': '123',
24
+ 'name': 'Test Stream',
25
+ 'type': 'WEB_DATA_STREAM',
26
+ 'measurement_id': 'G-XXXXXXXXXX',
27
+ 'url': 'https://example.com',
28
+ }]
29
+
30
+
31
+ @patch('ga_cli.decorators.AuthManager')
32
+ def test_datastreams_get_prints_measurement_id(
33
+ mock_auth, cli_runner, mock_datastream, mock_credentials_path
34
+ ):
35
+ mock_client = Mock()
36
+ mock_client.get_data_stream.return_value = mock_datastream
37
+ mock_auth.return_value.get_client.return_value = mock_client
38
+
39
+ result = cli_runner.invoke(
40
+ cli, ['--credentials', mock_credentials_path, 'datastreams', 'get', '987654', '123']
41
+ )
42
+
43
+ assert result.exit_code == 0, result.output
44
+ assert 'Measurement ID: G-XXXXXXXXXX' in result.output
45
+ mock_client.get_data_stream.assert_called_once_with(
46
+ name='properties/987654/dataStreams/123'
47
+ )
48
+
49
+
50
+ @patch('ga_cli.decorators.AuthManager')
51
+ def test_datastreams_create_builds_web_stream(
52
+ mock_auth, cli_runner, mock_datastream, mock_credentials_path
53
+ ):
54
+ """The DataStream proto sent to the API carries the type and URL"""
55
+ mock_client = Mock()
56
+ mock_client.create_data_stream.return_value = mock_datastream
57
+ mock_auth.return_value.get_client.return_value = mock_client
58
+
59
+ result = cli_runner.invoke(
60
+ cli, ['--credentials', mock_credentials_path, 'datastreams', 'create', '987654',
61
+ '--name', 'Main Website', '--url', 'https://example.com']
62
+ )
63
+
64
+ assert result.exit_code == 0, result.output
65
+ assert 'Stream ID: 123' in result.output
66
+ assert 'Measurement ID: G-XXXXXXXXXX' in result.output
67
+
68
+ kwargs = mock_client.create_data_stream.call_args.kwargs
69
+ assert kwargs['parent'] == 'properties/987654'
70
+ sent = kwargs['data_stream']
71
+ assert sent.display_name == 'Main Website'
72
+ assert sent.type_ == DataStream.DataStreamType.WEB_DATA_STREAM
73
+ assert sent.web_stream_data.default_uri == 'https://example.com'
74
+ # Round-trip through the wire format to prove the nested field is really set
75
+ assert DataStream.deserialize(DataStream.serialize(sent)).web_stream_data.default_uri == \
76
+ 'https://example.com'
77
+
78
+
79
+ def test_datastreams_create_rejects_bad_url(cli_runner, mock_credentials_path):
80
+ result = cli_runner.invoke(
81
+ cli, ['--credentials', mock_credentials_path, 'datastreams', 'create', '987654',
82
+ '--name', 'Main Website', '--url', 'example.com']
83
+ )
84
+
85
+ assert result.exit_code == 2
86
+ assert 'URL must start with http:// or https://' in result.output
@@ -1,7 +1,5 @@
1
1
  """Tests for error handling module"""
2
2
 
3
- import pytest
4
- from unittest.mock import Mock
5
3
  from google.api_core import exceptions
6
4
  from ga_cli.errors import get_friendly_error, ERROR_MESSAGES
7
5
 
@@ -0,0 +1,134 @@
1
+ """Tests for property commands"""
2
+
3
+ import json
4
+ from unittest.mock import Mock, patch
5
+ from google.analytics.admin_v1alpha.types import IndustryCategory
6
+ from ga_cli.cli import cli
7
+
8
+
9
+ @patch('ga_cli.decorators.AuthManager')
10
+ def test_properties_list(mock_auth, cli_runner, mock_property, mock_credentials_path):
11
+ """properties list filters by ancestor account and renders the rows"""
12
+ mock_client = Mock()
13
+ mock_client.list_properties.return_value = [mock_property]
14
+ mock_auth.return_value.get_client.return_value = mock_client
15
+
16
+ result = cli_runner.invoke(
17
+ cli, ['--credentials', mock_credentials_path, 'properties', 'list', '123456', '--format', 'json']
18
+ )
19
+
20
+ assert result.exit_code == 0, result.output
21
+ mock_client.list_properties.assert_called_once_with(
22
+ request={"filter": "ancestor:accounts/123456"}
23
+ )
24
+ data = json.loads(result.output)
25
+ assert data == [{
26
+ 'id': '987654',
27
+ 'name': 'Test Property',
28
+ 'type': 'PROPERTY_TYPE_ORDINARY',
29
+ 'timezone': 'America/Los_Angeles',
30
+ 'currency': 'USD',
31
+ }]
32
+
33
+
34
+ @patch('ga_cli.decorators.AuthManager')
35
+ def test_properties_get(mock_auth, cli_runner, mock_property, mock_credentials_path):
36
+ mock_client = Mock()
37
+ mock_client.get_property.return_value = mock_property
38
+ mock_auth.return_value.get_client.return_value = mock_client
39
+
40
+ result = cli_runner.invoke(
41
+ cli, ['--credentials', mock_credentials_path, 'properties', 'get', '987654']
42
+ )
43
+
44
+ assert result.exit_code == 0, result.output
45
+ assert 'Test Property' in result.output
46
+ assert 'TECHNOLOGY' in result.output
47
+ mock_client.get_property.assert_called_once_with(name='properties/987654')
48
+
49
+
50
+ @patch('ga_cli.decorators.AuthManager')
51
+ def test_properties_create_defaults(mock_auth, cli_runner, mock_property, mock_credentials_path):
52
+ """Default timezone, currency and industry are sent as a real Property proto"""
53
+ mock_client = Mock()
54
+ mock_client.create_property.return_value = mock_property
55
+ mock_auth.return_value.get_client.return_value = mock_client
56
+
57
+ result = cli_runner.invoke(
58
+ cli, ['--credentials', mock_credentials_path, 'properties', 'create', '123456',
59
+ '--name', 'My Site']
60
+ )
61
+
62
+ assert result.exit_code == 0, result.output
63
+ assert 'Created property: Test Property' in result.output
64
+ assert 'Property ID: 987654' in result.output
65
+
66
+ sent = mock_client.create_property.call_args.kwargs['property']
67
+ assert sent.parent == 'accounts/123456'
68
+ assert sent.display_name == 'My Site'
69
+ assert sent.time_zone == 'America/Los_Angeles'
70
+ assert sent.currency_code == 'USD'
71
+ assert sent.industry_category == IndustryCategory.OTHER
72
+
73
+
74
+ @patch('ga_cli.decorators.AuthManager')
75
+ def test_properties_create_industry_is_case_insensitive(
76
+ mock_auth, cli_runner, mock_property, mock_credentials_path
77
+ ):
78
+ mock_client = Mock()
79
+ mock_client.create_property.return_value = mock_property
80
+ mock_auth.return_value.get_client.return_value = mock_client
81
+
82
+ result = cli_runner.invoke(
83
+ cli, ['--credentials', mock_credentials_path, 'properties', 'create', '123456',
84
+ '--name', 'My Site', '--industry', 'technology',
85
+ '--timezone', 'Europe/Berlin', '--currency', 'EUR']
86
+ )
87
+
88
+ assert result.exit_code == 0, result.output
89
+ sent = mock_client.create_property.call_args.kwargs['property']
90
+ assert sent.industry_category == IndustryCategory.TECHNOLOGY
91
+ assert sent.time_zone == 'Europe/Berlin'
92
+ assert sent.currency_code == 'EUR'
93
+
94
+
95
+ def test_properties_create_rejects_unknown_industry(cli_runner, mock_credentials_path):
96
+ """An invalid industry is a usage error, not a KeyError from the API layer"""
97
+ result = cli_runner.invoke(
98
+ cli, ['--credentials', mock_credentials_path, 'properties', 'create', '123456',
99
+ '--name', 'My Site', '--industry', 'BOGUS']
100
+ )
101
+
102
+ assert result.exit_code == 2
103
+ assert "Invalid value for '--industry'" in result.output
104
+
105
+
106
+ @patch('ga_cli.decorators.AuthManager')
107
+ def test_properties_delete(mock_auth, cli_runner, mock_property, mock_credentials_path):
108
+ """delete is a soft delete and the output says so"""
109
+ mock_client = Mock()
110
+ mock_client.delete_property.return_value = mock_property
111
+ mock_auth.return_value.get_client.return_value = mock_client
112
+
113
+ result = cli_runner.invoke(
114
+ cli, ['--credentials', mock_credentials_path, 'properties', 'delete', '987654', '--yes']
115
+ )
116
+
117
+ assert result.exit_code == 0, result.output
118
+ assert 'moved to the trash' in result.output
119
+ mock_client.delete_property.assert_called_once_with(name='properties/987654')
120
+
121
+
122
+ @patch('ga_cli.decorators.AuthManager')
123
+ def test_properties_delete_aborts_without_confirmation(
124
+ mock_auth, cli_runner, mock_credentials_path
125
+ ):
126
+ mock_client = Mock()
127
+ mock_auth.return_value.get_client.return_value = mock_client
128
+
129
+ result = cli_runner.invoke(
130
+ cli, ['--credentials', mock_credentials_path, 'properties', 'delete', '987654'], input='n\n'
131
+ )
132
+
133
+ assert result.exit_code == 1
134
+ mock_client.delete_property.assert_not_called()
@@ -1,49 +0,0 @@
1
- """Tests for account commands"""
2
-
3
- from click.testing import CliRunner
4
- from unittest.mock import Mock, patch, MagicMock
5
- from ga_cli.cli import cli
6
-
7
-
8
- @patch('ga_cli.decorators.AuthManager')
9
- def test_accounts_list(mock_auth):
10
- """Test accounts list command"""
11
- runner = CliRunner()
12
-
13
- mock_account = Mock()
14
- mock_account.name = "accounts/123456"
15
- mock_account.display_name = "Test Account"
16
- mock_account.region_code = "US"
17
- mock_account.create_time = MagicMock()
18
- mock_account.create_time.__str__ = Mock(return_value="2023-01-01 00:00:00.000000")
19
-
20
- mock_client = Mock()
21
- mock_client.list_accounts.return_value = [mock_account]
22
- mock_auth.return_value.get_client.return_value = mock_client
23
-
24
- result = runner.invoke(cli, ['accounts', 'list'])
25
-
26
- assert 'Test Account' in result.output or result.exit_code == 1
27
-
28
-
29
- @patch('ga_cli.decorators.AuthManager')
30
- def test_accounts_get(mock_auth):
31
- """Test accounts get command"""
32
- runner = CliRunner()
33
-
34
- mock_account = Mock()
35
- mock_account.name = "accounts/123456"
36
- mock_account.display_name = "Test Account"
37
- mock_account.region_code = "US"
38
- mock_account.create_time = MagicMock()
39
- mock_account.create_time.__str__ = Mock(return_value="2023-01-01 00:00:00.000000")
40
- mock_account.update_time = MagicMock()
41
- mock_account.update_time.__str__ = Mock(return_value="2023-01-02 00:00:00.000000")
42
-
43
- mock_client = Mock()
44
- mock_client.get_account.return_value = mock_account
45
- mock_auth.return_value.get_client.return_value = mock_client
46
-
47
- result = runner.invoke(cli, ['accounts', 'get', '123456'])
48
-
49
- assert 'Test Account' in result.output or result.exit_code == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes