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.
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/PKG-INFO +27 -16
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/README.md +21 -11
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/PKG-INFO +27 -16
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/SOURCES.txt +3 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/requires.txt +1 -1
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/__init__.py +1 -1
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/config.py +37 -8
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/datastreams.py +1 -5
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/properties.py +19 -6
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/decorators.py +2 -3
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/output.py +3 -2
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/setup.py +5 -4
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/conftest.py +22 -5
- ga4_cli-0.2.0/tests/test_accounts.py +71 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_auth.py +1 -2
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_cli.py +2 -1
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_config.py +0 -2
- ga4_cli-0.2.0/tests/test_config_commands.py +71 -0
- ga4_cli-0.2.0/tests/test_datastreams.py +86 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/test_errors.py +0 -2
- ga4_cli-0.2.0/tests/test_properties.py +134 -0
- ga4_cli-0.1.4/tests/test_accounts.py +0 -49
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/LICENSE +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/dependency_links.txt +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/entry_points.txt +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga4_cli.egg-info/top_level.txt +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/auth.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/cli.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/__init__.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/commands/accounts.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/config.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/errors.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/__init__.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/json.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/formatters/table.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/logging_config.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/retry.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/ga_cli/validators.py +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/setup.cfg +0 -0
- {ga4_cli-0.1.4 → ga4_cli-0.2.0}/tests/__init__.py +0 -0
- {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.
|
|
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
|
-
|
|
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<
|
|
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
|
|
75
|
+
### Using pipx (recommended for a global command)
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
pipx install ga4-cli
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Using Docker (build locally)
|
|
79
82
|
|
|
80
|
-
|
|
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
|
-
#
|
|
96
|
-
curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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
|
|
31
|
+
### Using pipx (recommended for a global command)
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
pipx install ga4-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using Docker (build locally)
|
|
36
38
|
|
|
37
|
-
|
|
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
|
-
#
|
|
53
|
-
curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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<
|
|
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
|
|
75
|
+
### Using pipx (recommended for a global command)
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
pipx install ga4-cli
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Using Docker (build locally)
|
|
79
82
|
|
|
80
|
-
|
|
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
|
-
#
|
|
96
|
-
curl -L -o ga-cli.tar.gz https://github.com/sulimanbenhalim/ga-cli/releases/latest/download/ga-cli-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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
|
|
@@ -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("
|
|
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='
|
|
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='
|
|
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
|
-
"""
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,<
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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,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
|
|
21
|
+
assert __version__ in result.output
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def test_accounts_command_exists():
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
File without changes
|