snapctl 0.50.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of snapctl might be problematic. Click here for more details.
- snapctl/commands/byogs.py +2 -1
- snapctl/commands/byows.py +26 -6
- snapctl/commands/release_notes.py +7 -1
- snapctl/commands/snapend.py +18 -9
- snapctl/config/app.py +31 -0
- snapctl/config/constants.py +6 -2
- snapctl/data/releases/1.0.0.mdx +7 -0
- snapctl/data/releases/beta-0.51.0.mdx +6 -0
- snapctl/data/releases/beta-0.53.1.mdx +6 -0
- snapctl/main.py +27 -0
- snapctl/utils/helper.py +13 -2
- snapctl/utils/telemetry.py +160 -0
- {snapctl-0.50.0.dist-info → snapctl-1.0.0.dist-info}/METADATA +1 -1
- {snapctl-0.50.0.dist-info → snapctl-1.0.0.dist-info}/RECORD +17 -12
- {snapctl-0.50.0.dist-info → snapctl-1.0.0.dist-info}/LICENSE +0 -0
- {snapctl-0.50.0.dist-info → snapctl-1.0.0.dist-info}/WHEEL +0 -0
- {snapctl-0.50.0.dist-info → snapctl-1.0.0.dist-info}/entry_points.txt +0 -0
snapctl/commands/byogs.py
CHANGED
|
@@ -400,8 +400,9 @@ class ByoGs:
|
|
|
400
400
|
code=SNAPCTL_INPUT_ERROR)
|
|
401
401
|
# Check docker file path
|
|
402
402
|
if not self.skip_build and not self.docker_path_filename:
|
|
403
|
+
path_in_message = self.resources_path if self.resources_path else self.path
|
|
403
404
|
snapctl_error(
|
|
404
|
-
f"Unable to find {
|
|
405
|
+
f"Unable to find the Dockerfile at {path_in_message}", SNAPCTL_INPUT_ERROR)
|
|
405
406
|
elif self.subcommand == 'sync':
|
|
406
407
|
# Check path
|
|
407
408
|
if not self.skip_build and not self.path:
|
snapctl/commands/byows.py
CHANGED
|
@@ -20,7 +20,6 @@ from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
|
20
20
|
from snapctl.utils.helper import snapctl_error, snapctl_success, get_dot_snapser_dir
|
|
21
21
|
from snapctl.utils.echo import info, warning
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
class Byows:
|
|
25
24
|
"""
|
|
26
25
|
CLI commands exposed for a Bring your own Workstation
|
|
@@ -70,6 +69,10 @@ class Byows:
|
|
|
70
69
|
snapctl_error(
|
|
71
70
|
message="Missing Input --snapend-id=$your_snapend_id",
|
|
72
71
|
code=SNAPCTL_INPUT_ERROR)
|
|
72
|
+
if not Byows.is_valid_cluster_id(self.snapend_id):
|
|
73
|
+
snapctl_error(
|
|
74
|
+
message="Invalid value --snapend-id must be a valid Snapend ID, e.g., 'a1b2c3d4'",
|
|
75
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
73
76
|
if self.byosnap_id is None or self.byosnap_id == '':
|
|
74
77
|
snapctl_error(
|
|
75
78
|
message="Missing Input --byosnap-id=$your_byosnap_id",
|
|
@@ -390,7 +393,7 @@ class Byows:
|
|
|
390
393
|
self._handle_signal))
|
|
391
394
|
if hasattr(signal, "SIGBREAK"):
|
|
392
395
|
signal.signal(signal.SIGBREAK,
|
|
393
|
-
|
|
396
|
+
functools.partial(self._handle_signal))
|
|
394
397
|
# Set up port forward
|
|
395
398
|
self._setup_port_forward(
|
|
396
399
|
response_json['proxyPrivateKey'],
|
|
@@ -409,16 +412,15 @@ class Byows:
|
|
|
409
412
|
return snapctl_success(
|
|
410
413
|
message='complete', progress=progress)
|
|
411
414
|
snapctl_error(
|
|
412
|
-
message='
|
|
415
|
+
message='Attach failed.',
|
|
413
416
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
414
417
|
except HTTPError as http_err:
|
|
415
418
|
snapctl_error(
|
|
416
|
-
message=Byows._format_portal_http_error(
|
|
417
|
-
"Unable to setup port forward", http_err, res),
|
|
419
|
+
message=Byows._format_portal_http_error("Attach failed.", http_err, res),
|
|
418
420
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
419
421
|
except RequestException as e:
|
|
420
422
|
snapctl_error(
|
|
421
|
-
message=f"
|
|
423
|
+
message=f"Attach failed: {e}",
|
|
422
424
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
423
425
|
finally:
|
|
424
426
|
progress.stop()
|
|
@@ -463,3 +465,21 @@ class Byows:
|
|
|
463
465
|
code=SNAPCTL_BYOWS_RESET_ERROR, progress=progress)
|
|
464
466
|
finally:
|
|
465
467
|
progress.stop()
|
|
468
|
+
|
|
469
|
+
@staticmethod
|
|
470
|
+
def is_valid_cluster_id(cluster_id: str) -> bool:
|
|
471
|
+
"""
|
|
472
|
+
Check if the input is a valid cluster ID (Snapend ID).
|
|
473
|
+
"""
|
|
474
|
+
import re
|
|
475
|
+
if not cluster_id:
|
|
476
|
+
return False
|
|
477
|
+
|
|
478
|
+
pattern = "^[a-z0-9]+$"
|
|
479
|
+
if not re.match(pattern, cluster_id):
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
if len(cluster_id) != 8:
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
return True
|
|
@@ -35,9 +35,15 @@ class ReleaseNotes:
|
|
|
35
35
|
"""
|
|
36
36
|
print('== Releases ' + '=' * (92))
|
|
37
37
|
# List all resource files in snapctl.data.releases
|
|
38
|
+
final_list = []
|
|
38
39
|
for resource in pkg_resources.contents(snapctl.data.releases):
|
|
39
40
|
if resource.endswith('.mdx'):
|
|
40
|
-
|
|
41
|
+
final_list.append(resource.replace(
|
|
42
|
+
'.mdx', '').replace('.md', ''))
|
|
43
|
+
# Sort versions in descending order
|
|
44
|
+
final_list.sort(reverse=True)
|
|
45
|
+
for version in final_list:
|
|
46
|
+
print(f"- {version}")
|
|
41
47
|
print('=' * (104))
|
|
42
48
|
snapctl_success(message="List versions")
|
|
43
49
|
|
snapctl/commands/snapend.py
CHANGED
|
@@ -35,7 +35,7 @@ class Snapend:
|
|
|
35
35
|
'download', 'update', 'state'
|
|
36
36
|
]
|
|
37
37
|
DOWNLOAD_CATEGORY = [
|
|
38
|
-
'sdk', 'protos', 'snapend-manifest'
|
|
38
|
+
'sdk', 'protos', 'snapend-manifest', 'legacy-sdk'
|
|
39
39
|
]
|
|
40
40
|
CATEGORY_TYPE_SDK = [
|
|
41
41
|
# 'omni',
|
|
@@ -84,9 +84,9 @@ class Snapend:
|
|
|
84
84
|
self.manifest_path_filename: Union[str, None] = manifest_path_filename
|
|
85
85
|
self.force: bool = force
|
|
86
86
|
self.category: str = category
|
|
87
|
-
self.portal_category: Union[str, None] = Snapend._make_portal_category(
|
|
88
|
-
category_format)
|
|
89
87
|
self.category_format: str = category_format
|
|
88
|
+
self.portal_category: Union[str, None] = Snapend._make_portal_category(
|
|
89
|
+
category, category_format)
|
|
90
90
|
self.category_type: Union[str, None] = category_type
|
|
91
91
|
self.category_http_lib: Union[str, None] = category_http_lib
|
|
92
92
|
self.download_types: Union[
|
|
@@ -117,21 +117,29 @@ class Snapend:
|
|
|
117
117
|
return None
|
|
118
118
|
|
|
119
119
|
@staticmethod
|
|
120
|
-
def _make_portal_category(category_format: str):
|
|
120
|
+
def _make_portal_category(category: str, category_format: str):
|
|
121
121
|
'''
|
|
122
122
|
We have simplified the input for the user to only take the category as sdk
|
|
123
123
|
The portal server however expects us to pass client-sdk or server-sdk
|
|
124
124
|
Hence we need to do this
|
|
125
125
|
'''
|
|
126
|
-
if category_format in CLIENT_SDK_TYPES:
|
|
126
|
+
if category == 'legacy-sdk' and category_format in CLIENT_SDK_TYPES:
|
|
127
|
+
return 'legacy-client-sdk'
|
|
128
|
+
if category == 'legacy-sdk' and category_format in SERVER_SDK_TYPES:
|
|
129
|
+
return 'legacy-server-sdk'
|
|
130
|
+
if category == 'sdk' and category_format in CLIENT_SDK_TYPES:
|
|
127
131
|
return 'client-sdk'
|
|
128
|
-
if category_format in SERVER_SDK_TYPES:
|
|
132
|
+
if category == 'sdk' and category_format in SERVER_SDK_TYPES:
|
|
129
133
|
return 'server-sdk'
|
|
134
|
+
if category == 'protos' and category_format in PROTOS_TYPES:
|
|
135
|
+
return 'protos'
|
|
136
|
+
if category == 'snapend-manifest' and category_format in SNAPEND_MANIFEST_TYPES:
|
|
137
|
+
return 'snapend-manifest'
|
|
130
138
|
return None
|
|
131
139
|
|
|
132
140
|
@staticmethod
|
|
133
141
|
def _make_download_type(category: str):
|
|
134
|
-
if category
|
|
142
|
+
if category in ['sdk', 'legacy-sdk']:
|
|
135
143
|
return SDK_TYPES
|
|
136
144
|
if category == 'protos':
|
|
137
145
|
return PROTOS_TYPES
|
|
@@ -226,7 +234,7 @@ class Snapend:
|
|
|
226
234
|
if self.category == 'protos':
|
|
227
235
|
url += f"&subtype={self.category_type}"
|
|
228
236
|
# If client or server SDK, add sub type and auth type
|
|
229
|
-
if self.category
|
|
237
|
+
if self.category in ['sdk', 'legacy-sdk']:
|
|
230
238
|
url += "&subtype=" + \
|
|
231
239
|
f"{self.download_types[self.category_format]['subtype']}"
|
|
232
240
|
url += f"&access_type={SDK_ACCESS_AUTH_TYPE_LOOKUP[self.category_type]['access_type']}"
|
|
@@ -240,6 +248,7 @@ class Snapend:
|
|
|
240
248
|
# Customize snaps
|
|
241
249
|
if self.snaps:
|
|
242
250
|
url += f"&snaps={self.snaps}"
|
|
251
|
+
# info(f"Downloading from {url}")
|
|
243
252
|
res = requests.get(
|
|
244
253
|
url, headers={'api-key': self.api_key}, timeout=SERVER_CALL_TIMEOUT
|
|
245
254
|
)
|
|
@@ -401,7 +410,7 @@ class Snapend:
|
|
|
401
410
|
code=SNAPCTL_INPUT_ERROR
|
|
402
411
|
)
|
|
403
412
|
# Check the auth type
|
|
404
|
-
if self.category
|
|
413
|
+
if self.category in ['sdk', 'legacy-sdk']:
|
|
405
414
|
if not self.category_type:
|
|
406
415
|
snapctl_error(
|
|
407
416
|
message="Missing required parameter: --type",
|
snapctl/config/app.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Configuration by environment
|
|
3
|
+
'''
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
APP_CONFIG: Dict[str, Dict[str, str]] = {
|
|
7
|
+
'DEV': {
|
|
8
|
+
'AMPLITUDE_REGION': 'US',
|
|
9
|
+
'AMPLITUDE_API_KEY': 'ca863e91bfb3ce084e022920083f2898',
|
|
10
|
+
'TELEMETRY_ACTIVE': 'false',
|
|
11
|
+
'TELEMETRY_DRY_RUN': 'true',
|
|
12
|
+
},
|
|
13
|
+
'DEV_TWO': {
|
|
14
|
+
'AMPLITUDE_REGION': 'US',
|
|
15
|
+
'AMPLITUDE_API_KEY': 'ca863e91bfb3ce084e022920083f2898',
|
|
16
|
+
'TELEMETRY_ACTIVE': 'false',
|
|
17
|
+
'TELEMETRY_DRY_RUN': 'true',
|
|
18
|
+
},
|
|
19
|
+
'PLAYTEST': {
|
|
20
|
+
'AMPLITUDE_REGION': 'US',
|
|
21
|
+
'AMPLITUDE_API_KEY': 'ca863e91bfb3ce084e022920083f2898',
|
|
22
|
+
'TELEMETRY_ACTIVE': 'false',
|
|
23
|
+
'TELEMETRY_DRY_RUN': 'false',
|
|
24
|
+
},
|
|
25
|
+
'PROD': {
|
|
26
|
+
'AMPLITUDE_REGION': 'US',
|
|
27
|
+
'AMPLITUDE_API_KEY': '31fe2221f24fc30694cda777e98bd7a1',
|
|
28
|
+
'TELEMETRY_ACTIVE': 'true',
|
|
29
|
+
'TELEMETRY_DRY_RUN': 'false',
|
|
30
|
+
}
|
|
31
|
+
}
|
snapctl/config/constants.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Constants used by snapctl
|
|
3
3
|
"""
|
|
4
4
|
COMPANY_NAME = 'Snapser'
|
|
5
|
-
VERSION_PREFIX = '
|
|
6
|
-
VERSION = '0.
|
|
5
|
+
VERSION_PREFIX = ''
|
|
6
|
+
VERSION = '0.53.1'
|
|
7
7
|
CONFIG_FILE_MAC = '~/.snapser/config'
|
|
8
8
|
CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
|
|
9
9
|
|
|
@@ -13,6 +13,10 @@ URL_KEY = 'SNAPSER_URL_KEY'
|
|
|
13
13
|
CONFIG_PATH_KEY = 'SNAPSER_CONFIG_PATH'
|
|
14
14
|
SERVER_CALL_TIMEOUT = 300
|
|
15
15
|
|
|
16
|
+
# Telemetry
|
|
17
|
+
AMPLITUDE_HTTP_US = "https://api2.amplitude.com/2/httpapi"
|
|
18
|
+
AMPLITUDE_HTTP_EU = "https://api.eu.amplitude.com/2/httpapi"
|
|
19
|
+
|
|
16
20
|
# HTTP codes
|
|
17
21
|
HTTP_UNAUTHORIZED = 401
|
|
18
22
|
HTTP_FORBIDDEN = 403
|
snapctl/main.py
CHANGED
|
@@ -23,6 +23,7 @@ from snapctl.config.hashes import PROTOS_TYPES, SERVICE_IDS, \
|
|
|
23
23
|
SNAPEND_MANIFEST_TYPES, SDK_TYPES
|
|
24
24
|
from snapctl.utils.echo import error, success, info
|
|
25
25
|
from snapctl.utils.helper import validate_api_key
|
|
26
|
+
from snapctl.utils.telemetry import telemetry
|
|
26
27
|
|
|
27
28
|
######### Globals #########
|
|
28
29
|
|
|
@@ -90,6 +91,21 @@ def extract_config(extract_key: str, profile: Union[str, None] = None) -> object
|
|
|
90
91
|
return result
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def get_environment(api_key_value: Union[str, None]) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Returns the environment based on the api_key
|
|
97
|
+
"""
|
|
98
|
+
if api_key_value is None:
|
|
99
|
+
return 'UNKNOWN'
|
|
100
|
+
if api_key_value.startswith('dev_'):
|
|
101
|
+
return 'DEV'
|
|
102
|
+
if api_key_value.startswith('devtwo_'):
|
|
103
|
+
return 'DEV_TWO'
|
|
104
|
+
if api_key_value.startswith('playtest_'):
|
|
105
|
+
return 'PLAYTEST'
|
|
106
|
+
return 'PROD'
|
|
107
|
+
|
|
108
|
+
|
|
93
109
|
def get_base_url(api_key: Union[str, None]) -> str:
|
|
94
110
|
"""
|
|
95
111
|
Returns the base url based on the api_key
|
|
@@ -153,6 +169,7 @@ def default_context_callback(ctx: typer.Context):
|
|
|
153
169
|
ctx.obj['api_key'] = api_key_obj['value']
|
|
154
170
|
ctx.obj['api_key_location'] = api_key_obj['location']
|
|
155
171
|
ctx.obj['profile'] = DEFAULT_PROFILE
|
|
172
|
+
ctx.obj['environment'] = get_environment(api_key_obj['value'])
|
|
156
173
|
ctx.obj['base_url'] = get_base_url(api_key_obj['value'])
|
|
157
174
|
ctx.obj['base_snapend_url'] = get_base_snapend_url(api_key_obj['value'])
|
|
158
175
|
|
|
@@ -173,6 +190,7 @@ def api_key_context_callback(
|
|
|
173
190
|
ctx.obj['version'] = VERSION
|
|
174
191
|
ctx.obj['api_key'] = api_key
|
|
175
192
|
ctx.obj['api_key_location'] = 'command-line-argument'
|
|
193
|
+
ctx.obj['environment'] = get_environment(api_key)
|
|
176
194
|
ctx.obj['base_url'] = get_base_url(api_key)
|
|
177
195
|
|
|
178
196
|
|
|
@@ -205,6 +223,7 @@ def profile_context_callback(
|
|
|
205
223
|
ctx.obj['api_key'] = api_key_obj['value']
|
|
206
224
|
ctx.obj['api_key_location'] = api_key_obj['location']
|
|
207
225
|
ctx.obj['profile'] = profile if profile else DEFAULT_PROFILE
|
|
226
|
+
ctx.obj['environment'] = get_environment(api_key_obj['value'])
|
|
208
227
|
ctx.obj['base_url'] = get_base_url(api_key_obj['value'])
|
|
209
228
|
|
|
210
229
|
|
|
@@ -236,6 +255,7 @@ def common(
|
|
|
236
255
|
|
|
237
256
|
|
|
238
257
|
@app.command()
|
|
258
|
+
@telemetry("validate", subcommand_arg="subcommand")
|
|
239
259
|
def validate(
|
|
240
260
|
ctx: typer.Context,
|
|
241
261
|
api_key: Union[str, None] = typer.Option(
|
|
@@ -255,6 +275,7 @@ def validate(
|
|
|
255
275
|
|
|
256
276
|
|
|
257
277
|
@app.command()
|
|
278
|
+
@telemetry("release_notes", subcommand_arg="subcommand")
|
|
258
279
|
def release_notes(
|
|
259
280
|
ctx: typer.Context,
|
|
260
281
|
subcommand: str = typer.Argument(
|
|
@@ -283,6 +304,7 @@ def release_notes(
|
|
|
283
304
|
|
|
284
305
|
|
|
285
306
|
@app.command()
|
|
307
|
+
@telemetry("byogs", subcommand_arg="subcommand")
|
|
286
308
|
def byogs(
|
|
287
309
|
ctx: typer.Context,
|
|
288
310
|
# Required fields
|
|
@@ -352,6 +374,7 @@ def byogs(
|
|
|
352
374
|
|
|
353
375
|
|
|
354
376
|
@app.command()
|
|
377
|
+
@telemetry("byosnap", subcommand_arg="subcommand")
|
|
355
378
|
def byosnap(
|
|
356
379
|
ctx: typer.Context,
|
|
357
380
|
# Required fields
|
|
@@ -460,6 +483,7 @@ def byosnap(
|
|
|
460
483
|
|
|
461
484
|
|
|
462
485
|
@app.command()
|
|
486
|
+
@telemetry("game", subcommand_arg="subcommand")
|
|
463
487
|
def game(
|
|
464
488
|
ctx: typer.Context,
|
|
465
489
|
# Required fields
|
|
@@ -495,6 +519,7 @@ def game(
|
|
|
495
519
|
|
|
496
520
|
|
|
497
521
|
@app.command()
|
|
522
|
+
@telemetry("generate", subcommand_arg="subcommand")
|
|
498
523
|
def generate(
|
|
499
524
|
ctx: typer.Context,
|
|
500
525
|
# Required fields
|
|
@@ -541,6 +566,7 @@ def generate(
|
|
|
541
566
|
|
|
542
567
|
|
|
543
568
|
@app.command()
|
|
569
|
+
@telemetry("snapend", subcommand_arg="subcommand")
|
|
544
570
|
def snapend(
|
|
545
571
|
ctx: typer.Context,
|
|
546
572
|
# Required fields
|
|
@@ -684,6 +710,7 @@ def snapend(
|
|
|
684
710
|
|
|
685
711
|
|
|
686
712
|
@app.command()
|
|
713
|
+
@telemetry("byows", subcommand_arg="subcommand")
|
|
687
714
|
def byows(
|
|
688
715
|
ctx: typer.Context,
|
|
689
716
|
# Required fields
|
snapctl/utils/helper.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Helper functions for snapctl
|
|
3
3
|
"""
|
|
4
4
|
from typing import Union, Dict
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
import re
|
|
6
7
|
import platform
|
|
7
8
|
import os
|
|
@@ -14,7 +15,7 @@ from snapctl.config.constants import HTTP_NOT_FOUND, HTTP_FORBIDDEN, HTTP_UNAUTH
|
|
|
14
15
|
SERVER_CALL_TIMEOUT, SNAPCTL_CONFIGURATION_ERROR, SNAPCTL_SUCCESS
|
|
15
16
|
from snapctl.config.hashes import ARCHITECTURE_MAPPING
|
|
16
17
|
from snapctl.utils.echo import error, success
|
|
17
|
-
from
|
|
18
|
+
from snapctl.config.app import APP_CONFIG
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def validate_api_key(base_url: str, api_key: Union[str, None]) -> bool:
|
|
@@ -185,10 +186,20 @@ def check_use_containerd_snapshotter() -> bool:
|
|
|
185
186
|
except Exception:
|
|
186
187
|
return False
|
|
187
188
|
|
|
189
|
+
|
|
188
190
|
def get_dot_snapser_dir() -> Path:
|
|
189
191
|
"""
|
|
190
192
|
Returns the .snapser configuration directory, creating it if necessary.
|
|
191
193
|
"""
|
|
192
194
|
config_dir = Path.home() / ".snapser"
|
|
193
195
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
194
|
-
return config_dir
|
|
196
|
+
return config_dir
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_config_value(environment: str, key: str) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Returns the config value based on the environment.
|
|
202
|
+
"""
|
|
203
|
+
if environment == '' or environment not in APP_CONFIG or key not in APP_CONFIG[environment]:
|
|
204
|
+
return ''
|
|
205
|
+
return APP_CONFIG[environment][key]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Telemetry utilities for snapctl
|
|
3
|
+
'''
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import functools
|
|
7
|
+
import platform
|
|
8
|
+
import uuid
|
|
9
|
+
import hashlib
|
|
10
|
+
import requests
|
|
11
|
+
import typer
|
|
12
|
+
from snapctl.config.constants import AMPLITUDE_HTTP_US, AMPLITUDE_HTTP_EU
|
|
13
|
+
from snapctl.utils.helper import get_config_value
|
|
14
|
+
from snapctl.utils.echo import info
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ctx(ctx: Optional[typer.Context]) -> dict:
|
|
18
|
+
try:
|
|
19
|
+
return ctx.obj or {}
|
|
20
|
+
except Exception:
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _base_props(ctx: Optional[typer.Context]) -> Dict[str, Any]:
|
|
25
|
+
c = _ctx(ctx)
|
|
26
|
+
return {
|
|
27
|
+
"source": "snapctl",
|
|
28
|
+
"cli_version": c.get("version"),
|
|
29
|
+
"os": platform.system(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _device_id_from_ctx(ctx: Optional[typer.Context]) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Amplitude requires either user_id or device_id.
|
|
36
|
+
We derive a non-reversible device_id from the API key (if present).
|
|
37
|
+
"""
|
|
38
|
+
c = _ctx(ctx)
|
|
39
|
+
api_key = c.get("api_key") or ""
|
|
40
|
+
if api_key:
|
|
41
|
+
# hash + truncate to keep it compact but stable
|
|
42
|
+
h = hashlib.sha256(f"snapctl|{api_key}".encode("utf-8")).hexdigest()
|
|
43
|
+
return f"dev-{h[:32]}"
|
|
44
|
+
# fallback: hostname or a random-ish node id
|
|
45
|
+
return f"host-{platform.node() or uuid.getnode()}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _endpoint_from_env(ctx: Optional[typer.Context]) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Returns the Amplitude endpoint based on environment config.
|
|
51
|
+
"""
|
|
52
|
+
c = _ctx(ctx)
|
|
53
|
+
env = c.get("environment")
|
|
54
|
+
region = (get_config_value(env, "AMPLITUDE_REGION") or "US").upper()
|
|
55
|
+
return AMPLITUDE_HTTP_EU if region == "EU" else AMPLITUDE_HTTP_US
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_active(ctx: Optional[typer.Context]) -> tuple[bool, bool, Optional[str]]:
|
|
59
|
+
"""
|
|
60
|
+
Returns (telemetry_active, dry_run, api_key)
|
|
61
|
+
"""
|
|
62
|
+
c = _ctx(ctx)
|
|
63
|
+
env = c.get("environment")
|
|
64
|
+
api_key = get_config_value(env, "AMPLITUDE_API_KEY")
|
|
65
|
+
if not api_key or api_key == '':
|
|
66
|
+
return (False, False, None)
|
|
67
|
+
telemetry_active = get_config_value(env, "TELEMETRY_ACTIVE") == "true"
|
|
68
|
+
dry_run = get_config_value(env, "TELEMETRY_DRY_RUN") == "true"
|
|
69
|
+
return (telemetry_active, dry_run, api_key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _post_event(payload: dict, endpoint: str, timeout_s: float) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Post the event to Amplitude.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
requests.post(endpoint, json=payload, timeout=timeout_s)
|
|
78
|
+
except Exception:
|
|
79
|
+
# Never break the CLI
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def track_simple(
|
|
84
|
+
ctx: Optional[typer.Context],
|
|
85
|
+
*,
|
|
86
|
+
command: str,
|
|
87
|
+
sub: str,
|
|
88
|
+
result: str,
|
|
89
|
+
count: int = 1,
|
|
90
|
+
timeout_s: float = 2.0,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Minimal Amplitude event:
|
|
94
|
+
event_type = action
|
|
95
|
+
event_properties = { category, label, count, ...tiny base props }
|
|
96
|
+
"""
|
|
97
|
+
category = 'cli'
|
|
98
|
+
active, dry_run, api_key = _is_active(ctx)
|
|
99
|
+
if not active or not api_key:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
action = f"{command}_{sub}" if sub else command
|
|
103
|
+
props = {**_base_props(ctx)}
|
|
104
|
+
if dry_run:
|
|
105
|
+
info(
|
|
106
|
+
f"[telemetry:DRY-RUN] category={category} action={action} label={result} "
|
|
107
|
+
f"count={count} props={props}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
payload = {
|
|
111
|
+
"api_key": api_key,
|
|
112
|
+
"events": [{
|
|
113
|
+
"event_type": action,
|
|
114
|
+
"device_id": _device_id_from_ctx(ctx),
|
|
115
|
+
"event_properties": props,
|
|
116
|
+
}]
|
|
117
|
+
}
|
|
118
|
+
_post_event(payload, _endpoint_from_env(ctx), timeout_s)
|
|
119
|
+
|
|
120
|
+
# -------- Decorator to auto-track per-command result --------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def telemetry(command_name: str, subcommand_arg: Optional[str] = None):
|
|
124
|
+
"""
|
|
125
|
+
Decorator to track telemetry for a command function.
|
|
126
|
+
"""
|
|
127
|
+
def deco(fn):
|
|
128
|
+
@functools.wraps(fn)
|
|
129
|
+
def wrapper(*args, **kwargs):
|
|
130
|
+
ctx: Optional[typer.Context] = kwargs.get("ctx")
|
|
131
|
+
sub = (kwargs.get(subcommand_arg) if subcommand_arg else None)
|
|
132
|
+
label = "success" # default unless we see a failure
|
|
133
|
+
should_track_run = True
|
|
134
|
+
try:
|
|
135
|
+
result = fn(*args, **kwargs)
|
|
136
|
+
return result
|
|
137
|
+
except typer.Exit as e:
|
|
138
|
+
code = getattr(e, "exit_code", None)
|
|
139
|
+
# treat Exit(0/None) as success, anything else as failure
|
|
140
|
+
label = "success" if (code == 0 or code is None) else "failure"
|
|
141
|
+
# Now we only want to track if it was a success
|
|
142
|
+
# typer.Exit is called by us on user failure.
|
|
143
|
+
# If we start tracking this, bad actors can spam our telemetry.
|
|
144
|
+
should_track_run = not label == 'failure'
|
|
145
|
+
raise
|
|
146
|
+
except SystemExit as e:
|
|
147
|
+
# print('#1')
|
|
148
|
+
code = getattr(e, "code", None)
|
|
149
|
+
label = "success" if (code == 0 or code is None) else "failure"
|
|
150
|
+
raise
|
|
151
|
+
except Exception:
|
|
152
|
+
# print('#2')
|
|
153
|
+
label = "failure"
|
|
154
|
+
raise
|
|
155
|
+
finally:
|
|
156
|
+
if should_track_run:
|
|
157
|
+
track_simple(ctx, command=command_name,
|
|
158
|
+
sub=sub, result=label, count=1)
|
|
159
|
+
return wrapper
|
|
160
|
+
return deco
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
snapctl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
snapctl/__main__.py,sha256=43jKoTk8b85hk_MT6499N3ruHdEfM8WBImd_-3VzjI8,116
|
|
3
3
|
snapctl/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
snapctl/commands/byogs.py,sha256=
|
|
4
|
+
snapctl/commands/byogs.py,sha256=ZeSb6q8zaBCRgUe1MKo_zFvqU70k-tcMFFDqR0F5fGg,20176
|
|
5
5
|
snapctl/commands/byosnap.py,sha256=TAITdbB_FhGk7K94bPDDjykXbU9XwO9opW__qdeg0SQ,78187
|
|
6
|
-
snapctl/commands/byows.py,sha256=
|
|
6
|
+
snapctl/commands/byows.py,sha256=wbfCGbjigZo37mryvvE_VJ9bJWjIjcRBLAS7IT6VNrI,18906
|
|
7
7
|
snapctl/commands/game.py,sha256=lAABIWIibrwcqvpKvTy_7UzzLtbiP6gdk4_qPBHFKOI,5248
|
|
8
8
|
snapctl/commands/generate.py,sha256=9-NlZVQllBT2LZT_t9S3ztwtHzTbM-C8_x0f6z70U3g,5606
|
|
9
|
-
snapctl/commands/release_notes.py,sha256=
|
|
10
|
-
snapctl/commands/snapend.py,sha256=
|
|
9
|
+
snapctl/commands/release_notes.py,sha256=EMl-NK9-MSdRxiVyA0MLX4CBG0zijOctddEODQa5PZ4,2151
|
|
10
|
+
snapctl/commands/snapend.py,sha256=Fxaf-oy_EX_MYzjR1OVibeClr0UUM53PZllyh28WAEU,40015
|
|
11
11
|
snapctl/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
snapctl/config/
|
|
12
|
+
snapctl/config/app.py,sha256=U9Mm0BbGJoqcmBBam2tN7F-oTf_u9dK6EJsiB-yVvKg,888
|
|
13
|
+
snapctl/config/constants.py,sha256=UOVdr_aCoGeQhOWtZIge8k1OWWh8KZ5pfCOzcBj0eS8,3844
|
|
13
14
|
snapctl/config/endpoints.py,sha256=jD0n5ocJBbIkrb97F2_r9hqAHzUuddAqzqqBAPUKDTI,477
|
|
14
15
|
snapctl/config/hashes.py,sha256=3OKAyOxQPnn--_hvPIzFJnhC8NVfQK4WT-RH4PHEV1w,5242
|
|
15
16
|
snapctl/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -17,6 +18,7 @@ snapctl/data/profiles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
17
18
|
snapctl/data/profiles/snapser-byosnap-profile.json,sha256=c7oIzMJewVQaoLaXCBPXlH-k5cL9DCz6q4IixsWWD9c,2404
|
|
18
19
|
snapctl/data/profiles/snapser-byosnap-profile.yaml,sha256=vK5JqHa1f1_jlcW0m7qg-sKN9njE8PURJHzaRdvfEOY,3600
|
|
19
20
|
snapctl/data/profiles/snapser-byosnap-profile.yml,sha256=vK5JqHa1f1_jlcW0m7qg-sKN9njE8PURJHzaRdvfEOY,3600
|
|
21
|
+
snapctl/data/releases/1.0.0.mdx,sha256=UY6qLOFv7lGgBU2A6ROf4Q0WGW-JbYN2ZyhLO3JUIKs,215
|
|
20
22
|
snapctl/data/releases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
23
|
snapctl/data/releases/beta-0.46.0.mdx,sha256=_MJA7TnnJjAnNm_EOvoBiz7FjHwl4c49sYtZ9r4KZQE,2386
|
|
22
24
|
snapctl/data/releases/beta-0.46.4.mdx,sha256=3-ojdCyvax27sdd8nKmcP0zfCWEzi_edPbSlkFqla4o,173
|
|
@@ -29,14 +31,17 @@ snapctl/data/releases/beta-0.49.1.mdx,sha256=4EXupEegYffnL5bu_XWxGbzIPp9xOzl7t4x
|
|
|
29
31
|
snapctl/data/releases/beta-0.49.2.mdx,sha256=Vv_OIQyPieEGI9FgTUT0v5Gm0PC2JWa2FqcCUhnvx6s,176
|
|
30
32
|
snapctl/data/releases/beta-0.49.3.mdx,sha256=XmSfpiH-LqvKS2REJ5FGDcLgyEN-OcSzwrXNI_EhiGc,234
|
|
31
33
|
snapctl/data/releases/beta-0.50.0.mdx,sha256=_w9f1HjHqukQ8IaTfUdk2W3gBcSwjMyMA8-rjuUxYTo,399
|
|
32
|
-
snapctl/
|
|
34
|
+
snapctl/data/releases/beta-0.51.0.mdx,sha256=5_xY29NJtH3Qa92UycCudrwDkSlUHRVNnP4GE4hF7-Y,93
|
|
35
|
+
snapctl/data/releases/beta-0.53.1.mdx,sha256=b__2LPkDhU3cTw5UC19Hxv1rDz6BsSLeI6fhHjOCP08,149
|
|
36
|
+
snapctl/main.py,sha256=BdVjXyCX64d3JIhPjDo4QEGhos6RAcQqSdY3MHBQNc4,26264
|
|
33
37
|
snapctl/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
38
|
snapctl/types/definitions.py,sha256=EQzLeiXkJ8ISRlCqHMviNVsWWpmhWjpKaOBLdlvOTmY,644
|
|
35
39
|
snapctl/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
40
|
snapctl/utils/echo.py,sha256=9fxrPW9kNJwiJvJBFy9RlAP4qGRcPTOHfQb3pQh3CMg,1446
|
|
37
|
-
snapctl/utils/helper.py,sha256=
|
|
38
|
-
snapctl
|
|
39
|
-
snapctl-0.
|
|
40
|
-
snapctl-0.
|
|
41
|
-
snapctl-0.
|
|
42
|
-
snapctl-0.
|
|
41
|
+
snapctl/utils/helper.py,sha256=5c5jfAYD4WsvPpuCLMCz3tPvji28786_pD9TZkRSFIs,7179
|
|
42
|
+
snapctl/utils/telemetry.py,sha256=_VntKXUrKgJoLKCskH0Z5VhthiVo8CP572U0mcZIH4k,5028
|
|
43
|
+
snapctl-1.0.0.dist-info/LICENSE,sha256=6AcXm54KFSpmUI1ji9NIBd4Xl-DtjTqiyjBzfVb_CEk,2804
|
|
44
|
+
snapctl-1.0.0.dist-info/METADATA,sha256=nKmUTmbGrkJbu_WeY2HpA-mVdOeLypk6p3RSdjqi6n4,36274
|
|
45
|
+
snapctl-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
46
|
+
snapctl-1.0.0.dist-info/entry_points.txt,sha256=tkKW9MzmFdRs6Bgkv29G78i9WEBK4WIOWunPfe3t2Wg,44
|
|
47
|
+
snapctl-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|