hpde-analytics-cli 6.0.7__tar.gz → 6.0.9__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.
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/PKG-INFO +46 -7
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/README.md +44 -6
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/auth/oauth.py +13 -3
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/main.py +12 -7
- hpde_analytics_cli-6.0.9/hpde_analytics_cli/paths.py +123 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/PKG-INFO +46 -7
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/SOURCES.txt +2 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/requires.txt +1 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/pyproject.toml +2 -1
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_main.py +35 -18
- hpde_analytics_cli-6.0.9/tests/test_paths.py +168 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/LICENSE +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/__init__.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/api/__init__.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/api/client.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/auth/__init__.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/auth/credentials.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/integrations/__init__.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/integrations/email_populator.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/integrations/google_sheets.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/__init__.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/data_export.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/field_discovery.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/report_generator.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/dependency_links.txt +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/entry_points.txt +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/top_level.txt +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/setup.cfg +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_client.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_credentials.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_data_export.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_email_populator.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_google_sheets.py +0 -0
- {hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/tests/test_report_generator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hpde-analytics-cli
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.9
|
|
4
4
|
Summary: Data analysis and reporting tool for HPDE and Time Trials programs
|
|
5
5
|
Author-email: Kevin Homan <homan13@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -32,6 +32,7 @@ Requires-Dist: openpyxl>=3.1.0
|
|
|
32
32
|
Requires-Dist: keyring>=24.0.0
|
|
33
33
|
Requires-Dist: gspread>=6.0.0
|
|
34
34
|
Requires-Dist: google-auth>=2.22.0
|
|
35
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
35
36
|
Provides-Extra: dev
|
|
36
37
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
37
38
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -94,16 +95,25 @@ pip install hpde-analytics-cli
|
|
|
94
95
|
This stores your credentials securely in your system's keyring (macOS Keychain, Windows Credential Locker, or Linux Secret Service).
|
|
95
96
|
|
|
96
97
|
**Option B: Environment file (Fallback)**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
Edit `.env` and add your MotorsportsReg OAuth credentials:
|
|
98
|
+
|
|
99
|
+
Create a `.env` file in the directory you run the tool from:
|
|
101
100
|
```
|
|
102
101
|
MSR_CONSUMER_KEY=your_consumer_key_here
|
|
103
102
|
MSR_CONSUMER_SECRET=your_consumer_secret_here
|
|
104
103
|
MSR_BASE_URL=https://api.motorsportreg.com
|
|
105
104
|
MSR_CALLBACK_PORT=8089
|
|
106
105
|
```
|
|
106
|
+
(Working from a clone, `cp .env.example .env` gives you a starting point.)
|
|
107
|
+
|
|
108
|
+
Two locations are searched, in this order:
|
|
109
|
+
|
|
110
|
+
| Location | Purpose |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `.env` in the working directory, or any parent | Per-project settings |
|
|
113
|
+
| `.env` in the user config directory (below) | Your defaults across all directories |
|
|
114
|
+
|
|
115
|
+
The project file wins where both set the same key; the user file supplies anything it
|
|
116
|
+
leaves out.
|
|
107
117
|
|
|
108
118
|
2. Check credential status (optional):
|
|
109
119
|
```bash
|
|
@@ -198,6 +208,7 @@ hpde-analytics-cli/
|
|
|
198
208
|
├── hpde_analytics_cli/
|
|
199
209
|
│ ├── __init__.py
|
|
200
210
|
│ ├── main.py # CLI entry point
|
|
211
|
+
│ ├── paths.py # Config, token and output path resolution
|
|
201
212
|
│ ├── auth/
|
|
202
213
|
│ │ ├── __init__.py
|
|
203
214
|
│ │ ├── credentials.py # Secure credential management
|
|
@@ -211,10 +222,35 @@ hpde-analytics-cli/
|
|
|
211
222
|
│ ├── field_discovery.py # API field enumeration
|
|
212
223
|
│ └── report_generator.py # Excel report generation
|
|
213
224
|
├── output/ # Export and report output (not in version control)
|
|
214
|
-
├── tokens/ # OAuth access tokens (not in version control)
|
|
215
225
|
└── tests/ # Unit tests
|
|
216
226
|
```
|
|
217
227
|
|
|
228
|
+
## File Locations
|
|
229
|
+
|
|
230
|
+
Exports and reports are written relative to **the directory you run the command from**, so
|
|
231
|
+
the tool behaves the same whether installed from PyPI or run from a clone. OAuth tokens go
|
|
232
|
+
to a per-user config directory, so they survive upgrades and stay out of your project
|
|
233
|
+
folders.
|
|
234
|
+
|
|
235
|
+
| What | Location |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| Exports and reports | `./output/` in the current directory (override with `--output-dir`) |
|
|
238
|
+
| Field inventory | `./output/field_inventory.json` (override with `--output`) |
|
|
239
|
+
| OAuth access token | user config directory, below |
|
|
240
|
+
| `.env` | current directory or any parent, then the user config directory |
|
|
241
|
+
|
|
242
|
+
The user config directory is the native location for your platform:
|
|
243
|
+
|
|
244
|
+
| Platform | Path |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| Linux | `~/.config/hpde-analytics-cli/` |
|
|
247
|
+
| macOS | `~/Library/Application Support/hpde-analytics-cli/` |
|
|
248
|
+
| Windows | `%LOCALAPPDATA%\hpde-analytics-cli\` |
|
|
249
|
+
|
|
250
|
+
Set `HPDE_CONFIG_DIR` to override it. The token file is written readable only by you
|
|
251
|
+
(mode `0600` on POSIX systems). A token left by version 6.0.7 or earlier is moved to the
|
|
252
|
+
new location automatically on first run, so you will not need to re-authenticate.
|
|
253
|
+
|
|
218
254
|
## Export Structure
|
|
219
255
|
|
|
220
256
|
Each export creates two data sets:
|
|
@@ -313,10 +349,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
313
349
|
## Security Notes
|
|
314
350
|
|
|
315
351
|
**Important:**
|
|
316
|
-
- Never commit `.env`
|
|
352
|
+
- Never commit a `.env` file to version control
|
|
317
353
|
- OAuth consumer secrets and access tokens must be kept confidential
|
|
318
354
|
- The `.gitignore` file is configured to exclude these sensitive files automatically
|
|
319
355
|
- Use `hpde-analytics-cli --configure` to store credentials securely in your system keyring
|
|
356
|
+
- Access tokens live outside the project in the user config directory, written mode `0600`
|
|
357
|
+
- Exported data contains registrant names and email addresses; `output/` is gitignored, but
|
|
358
|
+
take the same care with it that you would with any roster
|
|
320
359
|
|
|
321
360
|
## Support
|
|
322
361
|
|
|
@@ -48,16 +48,25 @@ pip install hpde-analytics-cli
|
|
|
48
48
|
This stores your credentials securely in your system's keyring (macOS Keychain, Windows Credential Locker, or Linux Secret Service).
|
|
49
49
|
|
|
50
50
|
**Option B: Environment file (Fallback)**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
Edit `.env` and add your MotorsportsReg OAuth credentials:
|
|
51
|
+
|
|
52
|
+
Create a `.env` file in the directory you run the tool from:
|
|
55
53
|
```
|
|
56
54
|
MSR_CONSUMER_KEY=your_consumer_key_here
|
|
57
55
|
MSR_CONSUMER_SECRET=your_consumer_secret_here
|
|
58
56
|
MSR_BASE_URL=https://api.motorsportreg.com
|
|
59
57
|
MSR_CALLBACK_PORT=8089
|
|
60
58
|
```
|
|
59
|
+
(Working from a clone, `cp .env.example .env` gives you a starting point.)
|
|
60
|
+
|
|
61
|
+
Two locations are searched, in this order:
|
|
62
|
+
|
|
63
|
+
| Location | Purpose |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `.env` in the working directory, or any parent | Per-project settings |
|
|
66
|
+
| `.env` in the user config directory (below) | Your defaults across all directories |
|
|
67
|
+
|
|
68
|
+
The project file wins where both set the same key; the user file supplies anything it
|
|
69
|
+
leaves out.
|
|
61
70
|
|
|
62
71
|
2. Check credential status (optional):
|
|
63
72
|
```bash
|
|
@@ -152,6 +161,7 @@ hpde-analytics-cli/
|
|
|
152
161
|
├── hpde_analytics_cli/
|
|
153
162
|
│ ├── __init__.py
|
|
154
163
|
│ ├── main.py # CLI entry point
|
|
164
|
+
│ ├── paths.py # Config, token and output path resolution
|
|
155
165
|
│ ├── auth/
|
|
156
166
|
│ │ ├── __init__.py
|
|
157
167
|
│ │ ├── credentials.py # Secure credential management
|
|
@@ -165,10 +175,35 @@ hpde-analytics-cli/
|
|
|
165
175
|
│ ├── field_discovery.py # API field enumeration
|
|
166
176
|
│ └── report_generator.py # Excel report generation
|
|
167
177
|
├── output/ # Export and report output (not in version control)
|
|
168
|
-
├── tokens/ # OAuth access tokens (not in version control)
|
|
169
178
|
└── tests/ # Unit tests
|
|
170
179
|
```
|
|
171
180
|
|
|
181
|
+
## File Locations
|
|
182
|
+
|
|
183
|
+
Exports and reports are written relative to **the directory you run the command from**, so
|
|
184
|
+
the tool behaves the same whether installed from PyPI or run from a clone. OAuth tokens go
|
|
185
|
+
to a per-user config directory, so they survive upgrades and stay out of your project
|
|
186
|
+
folders.
|
|
187
|
+
|
|
188
|
+
| What | Location |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| Exports and reports | `./output/` in the current directory (override with `--output-dir`) |
|
|
191
|
+
| Field inventory | `./output/field_inventory.json` (override with `--output`) |
|
|
192
|
+
| OAuth access token | user config directory, below |
|
|
193
|
+
| `.env` | current directory or any parent, then the user config directory |
|
|
194
|
+
|
|
195
|
+
The user config directory is the native location for your platform:
|
|
196
|
+
|
|
197
|
+
| Platform | Path |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| Linux | `~/.config/hpde-analytics-cli/` |
|
|
200
|
+
| macOS | `~/Library/Application Support/hpde-analytics-cli/` |
|
|
201
|
+
| Windows | `%LOCALAPPDATA%\hpde-analytics-cli\` |
|
|
202
|
+
|
|
203
|
+
Set `HPDE_CONFIG_DIR` to override it. The token file is written readable only by you
|
|
204
|
+
(mode `0600` on POSIX systems). A token left by version 6.0.7 or earlier is moved to the
|
|
205
|
+
new location automatically on first run, so you will not need to re-authenticate.
|
|
206
|
+
|
|
172
207
|
## Export Structure
|
|
173
208
|
|
|
174
209
|
Each export creates two data sets:
|
|
@@ -267,10 +302,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
267
302
|
## Security Notes
|
|
268
303
|
|
|
269
304
|
**Important:**
|
|
270
|
-
- Never commit `.env`
|
|
305
|
+
- Never commit a `.env` file to version control
|
|
271
306
|
- OAuth consumer secrets and access tokens must be kept confidential
|
|
272
307
|
- The `.gitignore` file is configured to exclude these sensitive files automatically
|
|
273
308
|
- Use `hpde-analytics-cli --configure` to store credentials securely in your system keyring
|
|
309
|
+
- Access tokens live outside the project in the user config directory, written mode `0600`
|
|
310
|
+
- Exported data contains registrant names and email addresses; `output/` is gitignored, but
|
|
311
|
+
take the same care with it that you would with any roster
|
|
274
312
|
|
|
275
313
|
## Support
|
|
276
314
|
|
|
@@ -19,6 +19,8 @@ from urllib.parse import parse_qs, urlparse
|
|
|
19
19
|
|
|
20
20
|
from requests_oauthlib import OAuth1Session
|
|
21
21
|
|
|
22
|
+
from hpde_analytics_cli import paths
|
|
23
|
+
|
|
22
24
|
# Default callback port for local OAuth flow
|
|
23
25
|
DEFAULT_CALLBACK_PORT = 8089
|
|
24
26
|
|
|
@@ -147,8 +149,12 @@ class MSROAuth:
|
|
|
147
149
|
|
|
148
150
|
# Token storage
|
|
149
151
|
if token_file is None:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
self.token_file = paths.token_file()
|
|
153
|
+
# Only when using the default location: an explicit token_file (tests, or a
|
|
154
|
+
# caller managing its own state) must never have a stray file moved into it.
|
|
155
|
+
migrated_from = paths.migrate_legacy_token(self.token_file)
|
|
156
|
+
if migrated_from:
|
|
157
|
+
print(f"Moved access token from {migrated_from} to {self.token_file}")
|
|
152
158
|
else:
|
|
153
159
|
self.token_file = Path(token_file)
|
|
154
160
|
|
|
@@ -190,8 +196,9 @@ class MSROAuth:
|
|
|
190
196
|
return False
|
|
191
197
|
|
|
192
198
|
def _save_tokens(self) -> None:
|
|
193
|
-
"""Save access tokens to file."""
|
|
199
|
+
"""Save access tokens to file, readable only by the owner."""
|
|
194
200
|
self.token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
paths.restrict_permissions(self.token_file.parent)
|
|
195
202
|
|
|
196
203
|
data = {
|
|
197
204
|
"access_token": self.access_token,
|
|
@@ -203,6 +210,9 @@ class MSROAuth:
|
|
|
203
210
|
with open(self.token_file, "w") as f:
|
|
204
211
|
json.dump(data, f, indent=2)
|
|
205
212
|
|
|
213
|
+
# The file holds the access token and its secret; keep it off other accounts.
|
|
214
|
+
paths.restrict_permissions(self.token_file)
|
|
215
|
+
|
|
206
216
|
print(f"Access tokens saved to {self.token_file}")
|
|
207
217
|
|
|
208
218
|
def has_valid_tokens(self) -> bool:
|
|
@@ -14,7 +14,7 @@ from typing import Any, Dict, Optional
|
|
|
14
14
|
|
|
15
15
|
from dotenv import load_dotenv
|
|
16
16
|
|
|
17
|
-
from hpde_analytics_cli import __version__
|
|
17
|
+
from hpde_analytics_cli import __version__, paths
|
|
18
18
|
from hpde_analytics_cli.api.client import create_client_from_oauth
|
|
19
19
|
from hpde_analytics_cli.auth.credentials import CredentialManager
|
|
20
20
|
from hpde_analytics_cli.auth.oauth import MSROAuth, create_oauth_from_env
|
|
@@ -104,7 +104,7 @@ def handle_discover(oauth, args) -> None:
|
|
|
104
104
|
client = create_client_from_oauth(oauth, organization_id=args.org_id)
|
|
105
105
|
api_data = fetch_api_data(client, event_id=args.event_id, verbose=args.verbose)
|
|
106
106
|
|
|
107
|
-
output_path = Path(
|
|
107
|
+
output_path = Path(args.output)
|
|
108
108
|
run_field_discovery(api_data, output_path=str(output_path))
|
|
109
109
|
|
|
110
110
|
|
|
@@ -167,7 +167,7 @@ def handle_export(oauth, args) -> None:
|
|
|
167
167
|
if args.output_dir:
|
|
168
168
|
output_dir = Path(args.output_dir)
|
|
169
169
|
else:
|
|
170
|
-
output_dir =
|
|
170
|
+
output_dir = paths.default_output_dir()
|
|
171
171
|
|
|
172
172
|
exporter = DataExporter(output_dir=str(output_dir), name=args.name)
|
|
173
173
|
exported_files = exporter.export_all_data(
|
|
@@ -284,14 +284,19 @@ def handle_full_flow(oauth, args) -> None:
|
|
|
284
284
|
client = create_client_from_oauth(oauth, organization_id=args.org_id)
|
|
285
285
|
api_data = fetch_api_data(client, event_id=args.event_id, verbose=args.verbose)
|
|
286
286
|
|
|
287
|
-
output_path = Path(
|
|
287
|
+
output_path = Path(args.output)
|
|
288
288
|
run_field_discovery(api_data, output_path=str(output_path))
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
def load_environment(verbose: bool = False) -> None:
|
|
292
|
-
"""
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
"""
|
|
293
|
+
Load environment variables from .env files.
|
|
294
|
+
|
|
295
|
+
Loads the project .env (searched upward from the working directory) before the
|
|
296
|
+
per-user one. load_dotenv leaves already-set keys alone, so the project file wins and
|
|
297
|
+
the user file fills in whatever it omits.
|
|
298
|
+
"""
|
|
299
|
+
for env_path in paths.find_env_files():
|
|
295
300
|
load_dotenv(env_path)
|
|
296
301
|
if verbose:
|
|
297
302
|
print(f"Loaded environment from {env_path}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem path resolution.
|
|
3
|
+
|
|
4
|
+
Single owner for every user-facing path the CLI reads or writes.
|
|
5
|
+
|
|
6
|
+
Paths must never be derived from ``__file__``. For an installed package that resolves to
|
|
7
|
+
``site-packages``, which previously meant the user's ``.env`` was never read, OAuth tokens
|
|
8
|
+
were written where the next upgrade destroys them, and exported registration data landed
|
|
9
|
+
in a library directory. See CODE_REVIEW.md #2.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from dotenv import find_dotenv
|
|
17
|
+
from platformdirs import user_config_dir
|
|
18
|
+
|
|
19
|
+
APP_NAME = "hpde-analytics-cli"
|
|
20
|
+
|
|
21
|
+
#: Overrides the per-user config directory. Mainly for tests and sandboxed front-ends.
|
|
22
|
+
ENV_CONFIG_DIR = "HPDE_CONFIG_DIR"
|
|
23
|
+
|
|
24
|
+
ENV_FILENAME = ".env"
|
|
25
|
+
OUTPUT_DIRNAME = "output"
|
|
26
|
+
TOKEN_FILENAME = "access_token.json"
|
|
27
|
+
LEGACY_TOKEN_DIRNAME = "tokens"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def config_dir() -> Path:
|
|
31
|
+
"""
|
|
32
|
+
Per-user configuration directory.
|
|
33
|
+
|
|
34
|
+
Honours ``HPDE_CONFIG_DIR`` if set, otherwise the platform-native location:
|
|
35
|
+
``~/.config/hpde-analytics-cli`` on Linux, ``~/Library/Application Support/...`` on
|
|
36
|
+
macOS, ``%LOCALAPPDATA%\\...`` on Windows.
|
|
37
|
+
"""
|
|
38
|
+
override = os.environ.get(ENV_CONFIG_DIR)
|
|
39
|
+
if override:
|
|
40
|
+
return Path(override).expanduser()
|
|
41
|
+
return Path(user_config_dir(APP_NAME))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def token_file() -> Path:
|
|
45
|
+
"""Path to the stored OAuth access token."""
|
|
46
|
+
return config_dir() / TOKEN_FILENAME
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def default_output_dir() -> Path:
|
|
50
|
+
"""Default directory for exports and reports, relative to where the user is."""
|
|
51
|
+
return Path.cwd() / OUTPUT_DIRNAME
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_env_files() -> List[Path]:
|
|
55
|
+
"""
|
|
56
|
+
``.env`` files to load, in precedence order.
|
|
57
|
+
|
|
58
|
+
The project file comes first: ``load_dotenv`` does not overwrite keys that are already
|
|
59
|
+
set, so loading in this order gives a project-local ``.env`` precedence over the
|
|
60
|
+
per-user one, which then supplies anything the project file left out.
|
|
61
|
+
"""
|
|
62
|
+
found: List[Path] = []
|
|
63
|
+
|
|
64
|
+
project_env = find_dotenv(ENV_FILENAME, usecwd=True)
|
|
65
|
+
if project_env:
|
|
66
|
+
found.append(Path(project_env))
|
|
67
|
+
|
|
68
|
+
user_env = config_dir() / ENV_FILENAME
|
|
69
|
+
if user_env.exists() and user_env not in found:
|
|
70
|
+
found.append(user_env)
|
|
71
|
+
|
|
72
|
+
return found
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def legacy_token_file() -> Path:
|
|
76
|
+
"""
|
|
77
|
+
Where versions up to 6.0.7 stored the access token.
|
|
78
|
+
|
|
79
|
+
Derived from ``__file__`` the way the old code did, so it resolves to the repository
|
|
80
|
+
root for a source checkout and to ``site-packages`` for an installed package.
|
|
81
|
+
"""
|
|
82
|
+
return Path(__file__).parent.parent / LEGACY_TOKEN_DIRNAME / TOKEN_FILENAME
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def migrate_legacy_token(destination: Path) -> Optional[Path]:
|
|
86
|
+
"""
|
|
87
|
+
Move a token left by an older version into ``destination``.
|
|
88
|
+
|
|
89
|
+
Does nothing if the destination already exists or there is nothing to move, so it is
|
|
90
|
+
safe to call on every startup.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The path moved from, or None if nothing was migrated.
|
|
94
|
+
"""
|
|
95
|
+
if destination.exists():
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
legacy = legacy_token_file()
|
|
99
|
+
if not legacy.is_file():
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
destination.write_bytes(legacy.read_bytes())
|
|
105
|
+
restrict_permissions(destination)
|
|
106
|
+
legacy.unlink()
|
|
107
|
+
except OSError:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return legacy
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def restrict_permissions(path: Path) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Restrict a file to the owner (0600), or a directory to 0700.
|
|
116
|
+
|
|
117
|
+
Best effort: POSIX permissions do not map onto Windows ACLs, so failures are ignored
|
|
118
|
+
rather than breaking an otherwise working auth flow.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
os.chmod(path, 0o700 if path.is_dir() else 0o600)
|
|
122
|
+
except OSError:
|
|
123
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hpde-analytics-cli
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.9
|
|
4
4
|
Summary: Data analysis and reporting tool for HPDE and Time Trials programs
|
|
5
5
|
Author-email: Kevin Homan <homan13@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -32,6 +32,7 @@ Requires-Dist: openpyxl>=3.1.0
|
|
|
32
32
|
Requires-Dist: keyring>=24.0.0
|
|
33
33
|
Requires-Dist: gspread>=6.0.0
|
|
34
34
|
Requires-Dist: google-auth>=2.22.0
|
|
35
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
35
36
|
Provides-Extra: dev
|
|
36
37
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
37
38
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -94,16 +95,25 @@ pip install hpde-analytics-cli
|
|
|
94
95
|
This stores your credentials securely in your system's keyring (macOS Keychain, Windows Credential Locker, or Linux Secret Service).
|
|
95
96
|
|
|
96
97
|
**Option B: Environment file (Fallback)**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
Edit `.env` and add your MotorsportsReg OAuth credentials:
|
|
98
|
+
|
|
99
|
+
Create a `.env` file in the directory you run the tool from:
|
|
101
100
|
```
|
|
102
101
|
MSR_CONSUMER_KEY=your_consumer_key_here
|
|
103
102
|
MSR_CONSUMER_SECRET=your_consumer_secret_here
|
|
104
103
|
MSR_BASE_URL=https://api.motorsportreg.com
|
|
105
104
|
MSR_CALLBACK_PORT=8089
|
|
106
105
|
```
|
|
106
|
+
(Working from a clone, `cp .env.example .env` gives you a starting point.)
|
|
107
|
+
|
|
108
|
+
Two locations are searched, in this order:
|
|
109
|
+
|
|
110
|
+
| Location | Purpose |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `.env` in the working directory, or any parent | Per-project settings |
|
|
113
|
+
| `.env` in the user config directory (below) | Your defaults across all directories |
|
|
114
|
+
|
|
115
|
+
The project file wins where both set the same key; the user file supplies anything it
|
|
116
|
+
leaves out.
|
|
107
117
|
|
|
108
118
|
2. Check credential status (optional):
|
|
109
119
|
```bash
|
|
@@ -198,6 +208,7 @@ hpde-analytics-cli/
|
|
|
198
208
|
├── hpde_analytics_cli/
|
|
199
209
|
│ ├── __init__.py
|
|
200
210
|
│ ├── main.py # CLI entry point
|
|
211
|
+
│ ├── paths.py # Config, token and output path resolution
|
|
201
212
|
│ ├── auth/
|
|
202
213
|
│ │ ├── __init__.py
|
|
203
214
|
│ │ ├── credentials.py # Secure credential management
|
|
@@ -211,10 +222,35 @@ hpde-analytics-cli/
|
|
|
211
222
|
│ ├── field_discovery.py # API field enumeration
|
|
212
223
|
│ └── report_generator.py # Excel report generation
|
|
213
224
|
├── output/ # Export and report output (not in version control)
|
|
214
|
-
├── tokens/ # OAuth access tokens (not in version control)
|
|
215
225
|
└── tests/ # Unit tests
|
|
216
226
|
```
|
|
217
227
|
|
|
228
|
+
## File Locations
|
|
229
|
+
|
|
230
|
+
Exports and reports are written relative to **the directory you run the command from**, so
|
|
231
|
+
the tool behaves the same whether installed from PyPI or run from a clone. OAuth tokens go
|
|
232
|
+
to a per-user config directory, so they survive upgrades and stay out of your project
|
|
233
|
+
folders.
|
|
234
|
+
|
|
235
|
+
| What | Location |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| Exports and reports | `./output/` in the current directory (override with `--output-dir`) |
|
|
238
|
+
| Field inventory | `./output/field_inventory.json` (override with `--output`) |
|
|
239
|
+
| OAuth access token | user config directory, below |
|
|
240
|
+
| `.env` | current directory or any parent, then the user config directory |
|
|
241
|
+
|
|
242
|
+
The user config directory is the native location for your platform:
|
|
243
|
+
|
|
244
|
+
| Platform | Path |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| Linux | `~/.config/hpde-analytics-cli/` |
|
|
247
|
+
| macOS | `~/Library/Application Support/hpde-analytics-cli/` |
|
|
248
|
+
| Windows | `%LOCALAPPDATA%\hpde-analytics-cli\` |
|
|
249
|
+
|
|
250
|
+
Set `HPDE_CONFIG_DIR` to override it. The token file is written readable only by you
|
|
251
|
+
(mode `0600` on POSIX systems). A token left by version 6.0.7 or earlier is moved to the
|
|
252
|
+
new location automatically on first run, so you will not need to re-authenticate.
|
|
253
|
+
|
|
218
254
|
## Export Structure
|
|
219
255
|
|
|
220
256
|
Each export creates two data sets:
|
|
@@ -313,10 +349,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
313
349
|
## Security Notes
|
|
314
350
|
|
|
315
351
|
**Important:**
|
|
316
|
-
- Never commit `.env`
|
|
352
|
+
- Never commit a `.env` file to version control
|
|
317
353
|
- OAuth consumer secrets and access tokens must be kept confidential
|
|
318
354
|
- The `.gitignore` file is configured to exclude these sensitive files automatically
|
|
319
355
|
- Use `hpde-analytics-cli --configure` to store credentials securely in your system keyring
|
|
356
|
+
- Access tokens live outside the project in the user config directory, written mode `0600`
|
|
357
|
+
- Exported data contains registrant names and email addresses; `output/` is gitignored, but
|
|
358
|
+
take the same care with it that you would with any roster
|
|
320
359
|
|
|
321
360
|
## Support
|
|
322
361
|
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
hpde_analytics_cli/__init__.py
|
|
5
5
|
hpde_analytics_cli/main.py
|
|
6
|
+
hpde_analytics_cli/paths.py
|
|
6
7
|
hpde_analytics_cli.egg-info/PKG-INFO
|
|
7
8
|
hpde_analytics_cli.egg-info/SOURCES.txt
|
|
8
9
|
hpde_analytics_cli.egg-info/dependency_links.txt
|
|
@@ -27,4 +28,5 @@ tests/test_data_export.py
|
|
|
27
28
|
tests/test_email_populator.py
|
|
28
29
|
tests/test_google_sheets.py
|
|
29
30
|
tests/test_main.py
|
|
31
|
+
tests/test_paths.py
|
|
30
32
|
tests/test_report_generator.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hpde-analytics-cli"
|
|
7
|
-
version = "6.0.
|
|
7
|
+
version = "6.0.9" # x-release-please-version
|
|
8
8
|
description = "Data analysis and reporting tool for HPDE and Time Trials programs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -43,6 +43,7 @@ dependencies = [
|
|
|
43
43
|
"keyring>=24.0.0",
|
|
44
44
|
"gspread>=6.0.0",
|
|
45
45
|
"google-auth>=2.22.0",
|
|
46
|
+
"platformdirs>=4.0.0",
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
[project.optional-dependencies]
|
|
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
+
from hpde_analytics_cli import paths
|
|
10
11
|
from hpde_analytics_cli.main import (
|
|
11
12
|
create_parser,
|
|
12
13
|
handle_credential_commands,
|
|
@@ -211,35 +212,51 @@ class TestHandleCredentialCommands:
|
|
|
211
212
|
class TestLoadEnvironment:
|
|
212
213
|
"""Tests for load_environment function."""
|
|
213
214
|
|
|
215
|
+
@pytest.fixture(autouse=True)
|
|
216
|
+
def isolated_env(self, tmp_path, monkeypatch):
|
|
217
|
+
"""Run in an empty directory with the user config dir pointed somewhere empty."""
|
|
218
|
+
work = tmp_path / "work"
|
|
219
|
+
work.mkdir()
|
|
220
|
+
monkeypatch.chdir(work)
|
|
221
|
+
monkeypatch.setenv(paths.ENV_CONFIG_DIR, str(tmp_path / "config"))
|
|
222
|
+
return work
|
|
223
|
+
|
|
214
224
|
@patch("hpde_analytics_cli.main.load_dotenv")
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
mock_env_path.exists.return_value = True
|
|
220
|
-
mock_path.return_value.__truediv__.return_value.__truediv__.return_value = mock_env_path
|
|
225
|
+
def test_loads_env_from_working_directory(self, mock_load_dotenv, isolated_env):
|
|
226
|
+
"""A .env beside the user, not beside the installed package, is what gets read."""
|
|
227
|
+
env_file = isolated_env / ".env"
|
|
228
|
+
env_file.write_text("MSR_BASE_URL=https://example.test\n")
|
|
221
229
|
|
|
222
230
|
load_environment(verbose=False)
|
|
223
231
|
|
|
224
232
|
mock_load_dotenv.assert_called_once()
|
|
233
|
+
assert mock_load_dotenv.call_args[0][0].resolve() == env_file.resolve()
|
|
234
|
+
|
|
235
|
+
@patch("hpde_analytics_cli.main.load_dotenv")
|
|
236
|
+
def test_loads_project_env_before_user_env(self, mock_load_dotenv, isolated_env, tmp_path):
|
|
237
|
+
"""Project .env is loaded first so its keys win; the user file fills the rest."""
|
|
238
|
+
project_env = isolated_env / ".env"
|
|
239
|
+
project_env.write_text("MSR_BASE_URL=https://project.test\n")
|
|
240
|
+
config = tmp_path / "config"
|
|
241
|
+
config.mkdir()
|
|
242
|
+
user_env = config / ".env"
|
|
243
|
+
user_env.write_text("MSR_CALLBACK_PORT=9999\n")
|
|
244
|
+
|
|
245
|
+
load_environment(verbose=False)
|
|
246
|
+
|
|
247
|
+
loaded = [call[0][0].resolve() for call in mock_load_dotenv.call_args_list]
|
|
248
|
+
assert loaded == [project_env.resolve(), user_env.resolve()]
|
|
225
249
|
|
|
226
250
|
@patch("hpde_analytics_cli.main.load_dotenv")
|
|
227
|
-
def test_skips_when_no_env(self, mock_load_dotenv,
|
|
228
|
-
"""
|
|
229
|
-
# The function checks if .env exists before calling load_dotenv
|
|
230
|
-
# This test verifies load_environment handles missing .env gracefully
|
|
231
|
-
# Note: The actual .env check depends on the installed package location,
|
|
232
|
-
# so we just verify the function doesn't raise errors
|
|
251
|
+
def test_skips_when_no_env(self, mock_load_dotenv, isolated_env):
|
|
252
|
+
"""No .env anywhere is not an error, and nothing is loaded."""
|
|
233
253
|
load_environment(verbose=False)
|
|
234
|
-
|
|
254
|
+
mock_load_dotenv.assert_not_called()
|
|
235
255
|
|
|
236
256
|
@patch("hpde_analytics_cli.main.load_dotenv")
|
|
237
|
-
|
|
238
|
-
def test_verbose_output(self, mock_path, mock_load_dotenv, capsys):
|
|
257
|
+
def test_verbose_output(self, mock_load_dotenv, isolated_env, capsys):
|
|
239
258
|
"""Test verbose output when loading .env."""
|
|
240
|
-
|
|
241
|
-
mock_env_path.exists.return_value = True
|
|
242
|
-
mock_path.return_value.__truediv__.return_value.__truediv__.return_value = mock_env_path
|
|
259
|
+
(isolated_env / ".env").write_text("MSR_BASE_URL=https://example.test\n")
|
|
243
260
|
|
|
244
261
|
load_environment(verbose=True)
|
|
245
262
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for filesystem path resolution.
|
|
3
|
+
|
|
4
|
+
The property under test throughout is that no user-facing path depends on where the
|
|
5
|
+
package happens to be installed. See CODE_REVIEW.md #2.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from hpde_analytics_cli import paths
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def config_dir(tmp_path, monkeypatch):
|
|
20
|
+
"""Point the per-user config directory at a temporary directory."""
|
|
21
|
+
target = tmp_path / "config"
|
|
22
|
+
monkeypatch.setenv(paths.ENV_CONFIG_DIR, str(target))
|
|
23
|
+
return target
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestConfigDir:
|
|
27
|
+
def test_honours_env_override(self, tmp_path, monkeypatch):
|
|
28
|
+
monkeypatch.setenv(paths.ENV_CONFIG_DIR, str(tmp_path / "custom"))
|
|
29
|
+
assert paths.config_dir() == tmp_path / "custom"
|
|
30
|
+
|
|
31
|
+
def test_expands_user_in_override(self, monkeypatch):
|
|
32
|
+
monkeypatch.setenv(paths.ENV_CONFIG_DIR, "~/somewhere")
|
|
33
|
+
assert "~" not in str(paths.config_dir())
|
|
34
|
+
|
|
35
|
+
def test_falls_back_to_platform_location(self, monkeypatch):
|
|
36
|
+
monkeypatch.delenv(paths.ENV_CONFIG_DIR, raising=False)
|
|
37
|
+
resolved = paths.config_dir()
|
|
38
|
+
assert resolved.is_absolute()
|
|
39
|
+
assert paths.APP_NAME in str(resolved)
|
|
40
|
+
|
|
41
|
+
def test_not_inside_site_packages(self, monkeypatch):
|
|
42
|
+
"""The whole point of the module: never resolve into the install location."""
|
|
43
|
+
monkeypatch.delenv(paths.ENV_CONFIG_DIR, raising=False)
|
|
44
|
+
package_root = str(os.path.dirname(os.path.dirname(paths.__file__)))
|
|
45
|
+
assert not str(paths.config_dir()).startswith(package_root)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestTokenFile:
|
|
49
|
+
def test_lives_in_config_dir(self, config_dir):
|
|
50
|
+
assert paths.token_file() == config_dir / paths.TOKEN_FILENAME
|
|
51
|
+
|
|
52
|
+
def test_independent_of_working_directory(self, config_dir, tmp_path, monkeypatch):
|
|
53
|
+
monkeypatch.chdir(tmp_path)
|
|
54
|
+
first = paths.token_file()
|
|
55
|
+
sub = tmp_path / "elsewhere"
|
|
56
|
+
sub.mkdir()
|
|
57
|
+
monkeypatch.chdir(sub)
|
|
58
|
+
assert paths.token_file() == first
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestDefaultOutputDir:
|
|
62
|
+
def test_relative_to_working_directory(self, tmp_path, monkeypatch):
|
|
63
|
+
monkeypatch.chdir(tmp_path)
|
|
64
|
+
assert paths.default_output_dir() == tmp_path / paths.OUTPUT_DIRNAME
|
|
65
|
+
|
|
66
|
+
def test_follows_working_directory(self, tmp_path, monkeypatch):
|
|
67
|
+
sub = tmp_path / "events"
|
|
68
|
+
sub.mkdir()
|
|
69
|
+
monkeypatch.chdir(sub)
|
|
70
|
+
assert paths.default_output_dir() == sub / paths.OUTPUT_DIRNAME
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestFindEnvFiles:
|
|
74
|
+
def test_finds_project_env_in_cwd(self, tmp_path, monkeypatch, config_dir):
|
|
75
|
+
monkeypatch.chdir(tmp_path)
|
|
76
|
+
env = tmp_path / ".env"
|
|
77
|
+
env.write_text("MSR_BASE_URL=https://example.test\n")
|
|
78
|
+
assert env.resolve() in [p.resolve() for p in paths.find_env_files()]
|
|
79
|
+
|
|
80
|
+
def test_searches_upward_from_cwd(self, tmp_path, monkeypatch, config_dir):
|
|
81
|
+
env = tmp_path / ".env"
|
|
82
|
+
env.write_text("MSR_BASE_URL=https://example.test\n")
|
|
83
|
+
nested = tmp_path / "a" / "b"
|
|
84
|
+
nested.mkdir(parents=True)
|
|
85
|
+
monkeypatch.chdir(nested)
|
|
86
|
+
assert env.resolve() in [p.resolve() for p in paths.find_env_files()]
|
|
87
|
+
|
|
88
|
+
def test_includes_user_env(self, tmp_path, monkeypatch, config_dir):
|
|
89
|
+
monkeypatch.chdir(tmp_path)
|
|
90
|
+
config_dir.mkdir(parents=True)
|
|
91
|
+
user_env = config_dir / ".env"
|
|
92
|
+
user_env.write_text("MSR_BASE_URL=https://user.test\n")
|
|
93
|
+
assert user_env.resolve() in [p.resolve() for p in paths.find_env_files()]
|
|
94
|
+
|
|
95
|
+
def test_project_env_takes_precedence(self, tmp_path, monkeypatch, config_dir):
|
|
96
|
+
monkeypatch.chdir(tmp_path)
|
|
97
|
+
project_env = tmp_path / ".env"
|
|
98
|
+
project_env.write_text("MSR_BASE_URL=https://project.test\n")
|
|
99
|
+
config_dir.mkdir(parents=True)
|
|
100
|
+
(config_dir / ".env").write_text("MSR_BASE_URL=https://user.test\n")
|
|
101
|
+
|
|
102
|
+
found = paths.find_env_files()
|
|
103
|
+
assert len(found) == 2
|
|
104
|
+
assert found[0].resolve() == project_env.resolve()
|
|
105
|
+
|
|
106
|
+
def test_returns_empty_when_none_exist(self, tmp_path, monkeypatch, config_dir):
|
|
107
|
+
monkeypatch.chdir(tmp_path)
|
|
108
|
+
assert paths.find_env_files() == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestMigrateLegacyToken:
|
|
112
|
+
def test_no_op_when_nothing_to_migrate(self, config_dir, monkeypatch, tmp_path):
|
|
113
|
+
monkeypatch.setattr(paths, "legacy_token_file", lambda: tmp_path / "absent.json")
|
|
114
|
+
assert paths.migrate_legacy_token(paths.token_file()) is None
|
|
115
|
+
|
|
116
|
+
def test_no_op_when_destination_exists(self, config_dir, monkeypatch, tmp_path):
|
|
117
|
+
legacy = tmp_path / "legacy.json"
|
|
118
|
+
legacy.write_text('{"access_token": "old"}')
|
|
119
|
+
monkeypatch.setattr(paths, "legacy_token_file", lambda: legacy)
|
|
120
|
+
|
|
121
|
+
destination = paths.token_file()
|
|
122
|
+
destination.parent.mkdir(parents=True)
|
|
123
|
+
destination.write_text('{"access_token": "current"}')
|
|
124
|
+
|
|
125
|
+
assert paths.migrate_legacy_token(destination) is None
|
|
126
|
+
assert json.loads(destination.read_text())["access_token"] == "current"
|
|
127
|
+
assert legacy.exists(), "must not consume the legacy file when it was not used"
|
|
128
|
+
|
|
129
|
+
def test_moves_token_and_removes_original(self, config_dir, monkeypatch, tmp_path):
|
|
130
|
+
legacy = tmp_path / "legacy.json"
|
|
131
|
+
legacy.write_text('{"access_token": "abc", "access_token_secret": "xyz"}')
|
|
132
|
+
monkeypatch.setattr(paths, "legacy_token_file", lambda: legacy)
|
|
133
|
+
|
|
134
|
+
destination = paths.token_file()
|
|
135
|
+
assert paths.migrate_legacy_token(destination) == legacy
|
|
136
|
+
assert json.loads(destination.read_text())["access_token"] == "abc"
|
|
137
|
+
assert not legacy.exists()
|
|
138
|
+
|
|
139
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX permissions")
|
|
140
|
+
def test_migrated_token_is_owner_only(self, config_dir, monkeypatch, tmp_path):
|
|
141
|
+
legacy = tmp_path / "legacy.json"
|
|
142
|
+
legacy.write_text('{"access_token": "abc"}')
|
|
143
|
+
os.chmod(legacy, 0o644)
|
|
144
|
+
monkeypatch.setattr(paths, "legacy_token_file", lambda: legacy)
|
|
145
|
+
|
|
146
|
+
destination = paths.token_file()
|
|
147
|
+
paths.migrate_legacy_token(destination)
|
|
148
|
+
assert stat.S_IMODE(destination.stat().st_mode) == 0o600
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestRestrictPermissions:
|
|
152
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX permissions")
|
|
153
|
+
def test_file_becomes_owner_only(self, tmp_path):
|
|
154
|
+
target = tmp_path / "token.json"
|
|
155
|
+
target.write_text("{}")
|
|
156
|
+
os.chmod(target, 0o644)
|
|
157
|
+
paths.restrict_permissions(target)
|
|
158
|
+
assert stat.S_IMODE(target.stat().st_mode) == 0o600
|
|
159
|
+
|
|
160
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX permissions")
|
|
161
|
+
def test_directory_becomes_owner_only(self, tmp_path):
|
|
162
|
+
target = tmp_path / "cfg"
|
|
163
|
+
target.mkdir(mode=0o755)
|
|
164
|
+
paths.restrict_permissions(target)
|
|
165
|
+
assert stat.S_IMODE(target.stat().st_mode) == 0o700
|
|
166
|
+
|
|
167
|
+
def test_missing_path_is_ignored(self, tmp_path):
|
|
168
|
+
paths.restrict_permissions(tmp_path / "does-not-exist")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/auth/credentials.py
RENAMED
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/integrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/data_export.py
RENAMED
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/field_discovery.py
RENAMED
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli/utils/report_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{hpde_analytics_cli-6.0.7 → hpde_analytics_cli-6.0.9}/hpde_analytics_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|