snapctl 0.49.5__py3-none-any.whl → 0.53.1__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 +16 -2
- snapctl/commands/byosnap.py +53 -25
- snapctl/commands/byows.py +28 -10
- snapctl/commands/release_notes.py +7 -1
- snapctl/commands/snapend.py +13 -24
- snapctl/config/app.py +31 -0
- snapctl/config/constants.py +6 -2
- snapctl/data/releases/beta-0.50.0.mdx +10 -0
- snapctl/data/releases/beta-0.51.0.mdx +6 -0
- snapctl/data/releases/beta-0.53.1.mdx +6 -0
- snapctl/main.py +28 -2
- snapctl/utils/echo.py +19 -8
- snapctl/utils/helper.py +13 -2
- snapctl/utils/telemetry.py +160 -0
- {snapctl-0.49.5.dist-info → snapctl-0.53.1.dist-info}/METADATA +1 -1
- {snapctl-0.49.5.dist-info → snapctl-0.53.1.dist-info}/RECORD +19 -16
- snapctl/data/releases/beta-0.49.4.mdx +0 -8
- snapctl/data/releases/beta-0.49.5.mdx +0 -8
- {snapctl-0.49.5.dist-info → snapctl-0.53.1.dist-info}/LICENSE +0 -0
- {snapctl-0.49.5.dist-info → snapctl-0.53.1.dist-info}/WHEEL +0 -0
- {snapctl-0.49.5.dist-info → snapctl-0.53.1.dist-info}/entry_points.txt +0 -0
snapctl/commands/byogs.py
CHANGED
|
@@ -105,6 +105,16 @@ class ByoGs:
|
|
|
105
105
|
pass
|
|
106
106
|
return None
|
|
107
107
|
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _docker_supports_buildkit():
|
|
110
|
+
try:
|
|
111
|
+
version = subprocess.check_output(
|
|
112
|
+
["docker", "version", "--format", "{{.Server.Version}}"])
|
|
113
|
+
major, minor = map(int, version.decode().split(".")[:2])
|
|
114
|
+
return (major > 18) or (major == 18 and minor >= 9)
|
|
115
|
+
except Exception:
|
|
116
|
+
return False
|
|
117
|
+
|
|
108
118
|
def _check_dependencies(self) -> None:
|
|
109
119
|
progress = Progress(
|
|
110
120
|
SpinnerColumn(),
|
|
@@ -200,6 +210,10 @@ class ByoGs:
|
|
|
200
210
|
progress.add_task(
|
|
201
211
|
description='Building your snap...', total=None)
|
|
202
212
|
try:
|
|
213
|
+
env = os.environ.copy()
|
|
214
|
+
if ByoGs._docker_supports_buildkit():
|
|
215
|
+
info('Docker BuildKit is supported. Enabling it.')
|
|
216
|
+
env["DOCKER_BUILDKIT"] = "1"
|
|
203
217
|
# image_tag = f'{ByoGs.SID}.{self.tag}'
|
|
204
218
|
build_platform = ByoGs.DEFAULT_BUILD_PLATFORM
|
|
205
219
|
if len(self.token_parts) == 4:
|
|
@@ -217,7 +231,7 @@ class ByoGs:
|
|
|
217
231
|
# f"docker build --no-cache -t {tag} {path}"
|
|
218
232
|
'docker', 'build', '--platform', build_platform, '-t', self.tag,
|
|
219
233
|
'-f', self.docker_path_filename, self.path
|
|
220
|
-
], shell=True, check=False)
|
|
234
|
+
], shell=True, check=False, env=env)
|
|
221
235
|
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
222
236
|
else:
|
|
223
237
|
response = subprocess.run([
|
|
@@ -225,7 +239,7 @@ class ByoGs:
|
|
|
225
239
|
"docker build --platform " +
|
|
226
240
|
f"{build_platform} -t {self.tag} " +
|
|
227
241
|
f"-f {self.docker_path_filename} {self.path}"
|
|
228
|
-
], shell=True, check=False)
|
|
242
|
+
], shell=True, check=False, env=env)
|
|
229
243
|
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
230
244
|
if not response.returncode:
|
|
231
245
|
return snapctl_success(
|
snapctl/commands/byosnap.py
CHANGED
|
@@ -509,16 +509,19 @@ class ByoSnap:
|
|
|
509
509
|
outfile.write(line)
|
|
510
510
|
return True
|
|
511
511
|
except FileNotFoundError:
|
|
512
|
-
|
|
512
|
+
warning(
|
|
513
|
+
f"[ERROR] Could not find profile file: {resource_filename}")
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _docker_supports_buildkit():
|
|
518
|
+
try:
|
|
519
|
+
version = subprocess.check_output(
|
|
520
|
+
["docker", "version", "--format", "{{.Server.Version}}"])
|
|
521
|
+
major, minor = map(int, version.decode().split(".")[:2])
|
|
522
|
+
return (major > 18) or (major == 18 and minor >= 9)
|
|
523
|
+
except Exception:
|
|
513
524
|
return False
|
|
514
|
-
# @staticmethod
|
|
515
|
-
# def _handle_output_file(input_filepath, output_filepath) -> bool:
|
|
516
|
-
# file_written = False
|
|
517
|
-
# with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
|
|
518
|
-
# for line in in_file:
|
|
519
|
-
# outfile.write(line)
|
|
520
|
-
# file_written = True
|
|
521
|
-
# return file_written
|
|
522
525
|
|
|
523
526
|
def _get_profile_contents(self) -> dict:
|
|
524
527
|
"""
|
|
@@ -588,13 +591,15 @@ class ByoSnap:
|
|
|
588
591
|
code=SNAPCTL_INPUT_ERROR
|
|
589
592
|
)
|
|
590
593
|
# IMPORTANT: This is where the profile data is set and validated
|
|
594
|
+
#
|
|
595
|
+
# Update: June 2, 2025 - We removed the line that updated the self.platform_type
|
|
596
|
+
# self.platform_type = profile_data_obj['platform']
|
|
591
597
|
self.profile_data = profile_data_obj
|
|
592
598
|
ByoSnap._validate_byosnap_profile_data(self.profile_data)
|
|
593
599
|
# End: IMPORTANT: This is where the profile data is set
|
|
594
600
|
# Now apply the overrides
|
|
595
601
|
self.name = self.profile_data['name']
|
|
596
602
|
self.desc = self.profile_data['description']
|
|
597
|
-
self.platform_type = self.profile_data['platform']
|
|
598
603
|
self.language = self.profile_data['language']
|
|
599
604
|
self.prefix = self.profile_data['prefix']
|
|
600
605
|
# Setup the final ingress external port
|
|
@@ -716,6 +721,10 @@ class ByoSnap:
|
|
|
716
721
|
build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
|
|
717
722
|
if len(self.token_parts) == 4:
|
|
718
723
|
build_platform = self.token_parts[3]
|
|
724
|
+
if self.platform_type is not None:
|
|
725
|
+
build_platform = self.platform_type
|
|
726
|
+
# if len(self.token_parts) == 4:
|
|
727
|
+
# build_platform = self.token_parts[3]
|
|
719
728
|
progress = Progress(
|
|
720
729
|
SpinnerColumn(),
|
|
721
730
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -725,6 +734,10 @@ class ByoSnap:
|
|
|
725
734
|
progress.add_task(
|
|
726
735
|
description='Building your snap...', total=None)
|
|
727
736
|
try:
|
|
737
|
+
env = os.environ.copy()
|
|
738
|
+
if ByoSnap._docker_supports_buildkit():
|
|
739
|
+
info('Docker BuildKit is supported. Enabling it.')
|
|
740
|
+
env["DOCKER_BUILDKIT"] = "1"
|
|
728
741
|
# Warning check for architecture specific commands
|
|
729
742
|
info(f'Building on system architecture {sys_platform.machine()}')
|
|
730
743
|
check_response = check_dockerfile_architecture(
|
|
@@ -737,7 +750,7 @@ class ByoSnap:
|
|
|
737
750
|
# f"docker build --no-cache -t {remote_tag} {path}"
|
|
738
751
|
'docker', 'build', '--platform', build_platform, '-t', self.remote_tag,
|
|
739
752
|
'-f', self.docker_path_filename, self.path
|
|
740
|
-
], shell=True, check=False)
|
|
753
|
+
], shell=True, check=False, env=env)
|
|
741
754
|
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
742
755
|
else:
|
|
743
756
|
response = subprocess.run([
|
|
@@ -745,7 +758,7 @@ class ByoSnap:
|
|
|
745
758
|
"docker build --platform " +
|
|
746
759
|
f"{build_platform} -t {self.remote_tag} " +
|
|
747
760
|
f"-f {self.docker_path_filename} {self.path}"
|
|
748
|
-
], shell=True, check=False)
|
|
761
|
+
], shell=True, check=False, env=env)
|
|
749
762
|
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
750
763
|
if not response.returncode:
|
|
751
764
|
return snapctl_success(
|
|
@@ -943,15 +956,21 @@ class ByoSnap:
|
|
|
943
956
|
if self.path is None and self.resources_path is None:
|
|
944
957
|
snapctl_error(
|
|
945
958
|
"Missing one of: path or resources-path parameter", SNAPCTL_INPUT_ERROR)
|
|
946
|
-
if not self.tag:
|
|
947
|
-
snapctl_error("Missing tag", SNAPCTL_INPUT_ERROR)
|
|
948
|
-
if
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
959
|
+
if not self.tag and not self.version:
|
|
960
|
+
snapctl_error("Missing tag or version", SNAPCTL_INPUT_ERROR)
|
|
961
|
+
if self.tag:
|
|
962
|
+
if len(self.tag.split()) > 1 or \
|
|
963
|
+
len(self.tag) > ByoSnap.TAG_CHARACTER_LIMIT:
|
|
964
|
+
snapctl_error(
|
|
965
|
+
"Tag should be a single word with maximum of " +
|
|
966
|
+
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
967
|
+
SNAPCTL_INPUT_ERROR
|
|
968
|
+
)
|
|
969
|
+
if self.version:
|
|
970
|
+
pattern = r'^v\d+\.\d+\.\d+$'
|
|
971
|
+
if not re.match(pattern, self.version):
|
|
972
|
+
snapctl_error(message="Version should be in the format vX.X.X",
|
|
973
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
955
974
|
elif self.subcommand == 'publish-version':
|
|
956
975
|
# Setup
|
|
957
976
|
self._setup_token_and_token_parts(
|
|
@@ -1127,6 +1146,9 @@ class ByoSnap:
|
|
|
1127
1146
|
progress.add_task(
|
|
1128
1147
|
description='Uploading your BYOSnap Docs...', total=None)
|
|
1129
1148
|
try:
|
|
1149
|
+
upload_tag = self.tag
|
|
1150
|
+
if not upload_tag:
|
|
1151
|
+
upload_tag = self.version
|
|
1130
1152
|
if self.resources_path:
|
|
1131
1153
|
base_dir = self.resources_path
|
|
1132
1154
|
else:
|
|
@@ -1141,7 +1163,7 @@ class ByoSnap:
|
|
|
1141
1163
|
info(f'Uploading swagger.json at {swagger_file}')
|
|
1142
1164
|
url = (
|
|
1143
1165
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
1144
|
-
f"{self.byosnap_id}/docs/{
|
|
1166
|
+
f"{self.byosnap_id}/docs/{upload_tag}/openapispec"
|
|
1145
1167
|
)
|
|
1146
1168
|
test_res = requests.post(
|
|
1147
1169
|
url, files={"attachment": attachment_file},
|
|
@@ -1181,7 +1203,7 @@ class ByoSnap:
|
|
|
1181
1203
|
with open(readme_file, "rb") as attachment_file:
|
|
1182
1204
|
url = (
|
|
1183
1205
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
1184
|
-
f"{self.byosnap_id}/docs/{
|
|
1206
|
+
f"{self.byosnap_id}/docs/{upload_tag}/markdown"
|
|
1185
1207
|
)
|
|
1186
1208
|
test_res = requests.post(
|
|
1187
1209
|
url, files={"attachment": attachment_file},
|
|
@@ -1213,7 +1235,7 @@ class ByoSnap:
|
|
|
1213
1235
|
with open(file_path, "rb") as attachment_file:
|
|
1214
1236
|
url = (
|
|
1215
1237
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
1216
|
-
f"{self.byosnap_id}/docs/{
|
|
1238
|
+
f"{self.byosnap_id}/docs/{upload_tag}/tools"
|
|
1217
1239
|
)
|
|
1218
1240
|
test_res = requests.post(
|
|
1219
1241
|
url, files={"attachment": attachment_file},
|
|
@@ -1370,7 +1392,11 @@ class ByoSnap:
|
|
|
1370
1392
|
"prod_template": prod_template,
|
|
1371
1393
|
# Currently not supported so we are just hardcoding an empty list
|
|
1372
1394
|
"egress": {"ports": []},
|
|
1395
|
+
# Platform override
|
|
1373
1396
|
}
|
|
1397
|
+
if self.platform_type is not None:
|
|
1398
|
+
payload['platform_override'] = self.platform_type
|
|
1399
|
+
|
|
1374
1400
|
res = requests.post(
|
|
1375
1401
|
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
|
|
1376
1402
|
json=payload, headers={'api-key': self.api_key},
|
|
@@ -1452,6 +1478,8 @@ class ByoSnap:
|
|
|
1452
1478
|
# Currently not supported so we are just hardcoding an empty list
|
|
1453
1479
|
"egress": {"ports": []},
|
|
1454
1480
|
}
|
|
1481
|
+
if self.platform_type is not None:
|
|
1482
|
+
payload['platform_override'] = self.platform_type
|
|
1455
1483
|
res = requests.patch(
|
|
1456
1484
|
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
|
|
1457
1485
|
json=payload, headers={'api-key': self.api_key},
|
|
@@ -1504,7 +1532,7 @@ class ByoSnap:
|
|
|
1504
1532
|
"service_id": self.byosnap_id,
|
|
1505
1533
|
"name": self.name,
|
|
1506
1534
|
"description": self.desc,
|
|
1507
|
-
"platform": self.
|
|
1535
|
+
"platform": self.profile_data['platform'],
|
|
1508
1536
|
"language": self.language,
|
|
1509
1537
|
}
|
|
1510
1538
|
res = requests.post(
|
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",
|
|
@@ -99,12 +102,11 @@ class Byows:
|
|
|
99
102
|
def _get_export_commands(snap_ids, port):
|
|
100
103
|
'''
|
|
101
104
|
Generate export commands for the given snap IDs and port.
|
|
102
|
-
Replace hyphens with underscores in environment variable names.
|
|
103
105
|
'''
|
|
104
106
|
env_vars = []
|
|
105
107
|
|
|
106
108
|
for snap_id in snap_ids:
|
|
107
|
-
upper_id = snap_id.upper()
|
|
109
|
+
upper_id = snap_id.upper()
|
|
108
110
|
env_vars.append(f"SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
109
111
|
env_vars.append(
|
|
110
112
|
f"SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
|
|
@@ -122,12 +124,11 @@ class Byows:
|
|
|
122
124
|
def _generate_env_file(snap_ids, port):
|
|
123
125
|
'''
|
|
124
126
|
Generate an environment file with the given snap IDs and port.
|
|
125
|
-
Replace hyphens with underscores in environment variable names.
|
|
126
127
|
'''
|
|
127
128
|
env_lines = []
|
|
128
129
|
|
|
129
130
|
for snap_id in snap_ids:
|
|
130
|
-
upper_id = snap_id.upper()
|
|
131
|
+
upper_id = snap_id.upper()
|
|
131
132
|
env_lines.append(
|
|
132
133
|
f"export SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
133
134
|
env_lines.append(
|
|
@@ -392,7 +393,7 @@ class Byows:
|
|
|
392
393
|
self._handle_signal))
|
|
393
394
|
if hasattr(signal, "SIGBREAK"):
|
|
394
395
|
signal.signal(signal.SIGBREAK,
|
|
395
|
-
|
|
396
|
+
functools.partial(self._handle_signal))
|
|
396
397
|
# Set up port forward
|
|
397
398
|
self._setup_port_forward(
|
|
398
399
|
response_json['proxyPrivateKey'],
|
|
@@ -411,16 +412,15 @@ class Byows:
|
|
|
411
412
|
return snapctl_success(
|
|
412
413
|
message='complete', progress=progress)
|
|
413
414
|
snapctl_error(
|
|
414
|
-
message='
|
|
415
|
+
message='Attach failed.',
|
|
415
416
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
416
417
|
except HTTPError as http_err:
|
|
417
418
|
snapctl_error(
|
|
418
|
-
message=Byows._format_portal_http_error(
|
|
419
|
-
"Unable to setup port forward", http_err, res),
|
|
419
|
+
message=Byows._format_portal_http_error("Attach failed.", http_err, res),
|
|
420
420
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
421
421
|
except RequestException as e:
|
|
422
422
|
snapctl_error(
|
|
423
|
-
message=f"
|
|
423
|
+
message=f"Attach failed: {e}",
|
|
424
424
|
code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
|
|
425
425
|
finally:
|
|
426
426
|
progress.stop()
|
|
@@ -465,3 +465,21 @@ class Byows:
|
|
|
465
465
|
code=SNAPCTL_BYOWS_RESET_ERROR, progress=progress)
|
|
466
466
|
finally:
|
|
467
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
|
@@ -22,7 +22,7 @@ from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
|
22
22
|
SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR
|
|
23
23
|
from snapctl.config.hashes import PROTOS_TYPES, CLIENT_SDK_TYPES, SERVER_SDK_TYPES, \
|
|
24
24
|
SNAPEND_MANIFEST_TYPES, SDK_TYPES, SDK_ACCESS_AUTH_TYPE_LOOKUP
|
|
25
|
-
from snapctl.utils.echo import error, success, info
|
|
25
|
+
from snapctl.utils.echo import error, success, info
|
|
26
26
|
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
27
27
|
|
|
28
28
|
|
|
@@ -85,20 +85,16 @@ class Snapend:
|
|
|
85
85
|
self.force: bool = force
|
|
86
86
|
self.category: str = category
|
|
87
87
|
self.category_format: str = category_format
|
|
88
|
+
self.portal_category: Union[str, None] = Snapend._make_portal_category(
|
|
89
|
+
category, category_format)
|
|
88
90
|
self.category_type: Union[str, None] = category_type
|
|
89
91
|
self.category_http_lib: Union[str, None] = category_http_lib
|
|
90
|
-
self.out_path: Union[str, None] = out_path
|
|
91
|
-
self.snaps: Union[str, None] = snaps
|
|
92
|
-
self.blocking: bool = blocking
|
|
93
|
-
# Values below are derived values
|
|
94
|
-
# portal_category: This is purely because we have simplified the input for the user
|
|
95
|
-
# Only used for a reverse lookup to pass the category to the portal server
|
|
96
|
-
self.portal_category: Union[str, None] = Snapend._make_portal_category(
|
|
97
|
-
category_format)
|
|
98
|
-
# download_types: This stores our own internal mapping of the category to the type
|
|
99
92
|
self.download_types: Union[
|
|
100
93
|
Dict[str, Dict[str, str]], None
|
|
101
94
|
] = Snapend._make_download_type(category)
|
|
95
|
+
self.out_path: Union[str, None] = out_path
|
|
96
|
+
self.snaps: Union[str, None] = snaps
|
|
97
|
+
# Values below are derived values
|
|
102
98
|
self.manifest_file_name: Union[str, None] = Snapend._get_manifest_file_name(
|
|
103
99
|
manifest_path_filename
|
|
104
100
|
)
|
|
@@ -106,6 +102,7 @@ class Snapend:
|
|
|
106
102
|
byosnaps) if byosnaps else None
|
|
107
103
|
self.byogs_list: Union[str, None] = Snapend._make_byogs_list(
|
|
108
104
|
byogs) if byogs else None
|
|
105
|
+
self.blocking: bool = blocking
|
|
109
106
|
# Validate input
|
|
110
107
|
self.validate_input()
|
|
111
108
|
|
|
@@ -120,19 +117,19 @@ class Snapend:
|
|
|
120
117
|
return None
|
|
121
118
|
|
|
122
119
|
@staticmethod
|
|
123
|
-
def _make_portal_category(category_format: str):
|
|
120
|
+
def _make_portal_category(category: str, category_format: str):
|
|
124
121
|
'''
|
|
125
122
|
We have simplified the input for the user to only take the category as sdk
|
|
126
123
|
The portal server however expects us to pass client-sdk or server-sdk
|
|
127
124
|
Hence we need to do this
|
|
128
125
|
'''
|
|
129
|
-
if category_format in CLIENT_SDK_TYPES:
|
|
126
|
+
if category == 'sdk' and category_format in CLIENT_SDK_TYPES:
|
|
130
127
|
return 'client-sdk'
|
|
131
|
-
if category_format in SERVER_SDK_TYPES:
|
|
128
|
+
if category == 'sdk' and category_format in SERVER_SDK_TYPES:
|
|
132
129
|
return 'server-sdk'
|
|
133
|
-
if category_format in PROTOS_TYPES:
|
|
130
|
+
if category == 'protos' and category_format in PROTOS_TYPES:
|
|
134
131
|
return 'protos'
|
|
135
|
-
if category_format in SNAPEND_MANIFEST_TYPES:
|
|
132
|
+
if category == 'snapend-manifest' and category_format in SNAPEND_MANIFEST_TYPES:
|
|
136
133
|
return 'snapend-manifest'
|
|
137
134
|
return None
|
|
138
135
|
|
|
@@ -247,8 +244,6 @@ class Snapend:
|
|
|
247
244
|
# Customize snaps
|
|
248
245
|
if self.snaps:
|
|
249
246
|
url += f"&snaps={self.snaps}"
|
|
250
|
-
if "None" in url:
|
|
251
|
-
warning('Detecting a potential issue in the download URL.')
|
|
252
247
|
res = requests.get(
|
|
253
248
|
url, headers={'api-key': self.api_key}, timeout=SERVER_CALL_TIMEOUT
|
|
254
249
|
)
|
|
@@ -400,14 +395,8 @@ class Snapend:
|
|
|
400
395
|
code=SNAPCTL_INPUT_ERROR)
|
|
401
396
|
if self.category not in Snapend.DOWNLOAD_TYPE_NOT_REQUIRED and \
|
|
402
397
|
(self.download_types is None or self.category_format not in self.download_types):
|
|
403
|
-
message = "Invalid format. Valid formats are " + \
|
|
404
|
-
"(req: --category sdk|protos|snapend-manifest --format " + \
|
|
405
|
-
"sdk(" + ", ".join(SDK_TYPES.keys()) + \
|
|
406
|
-
") | protos(" + ", ".join(PROTOS_TYPES.keys()) + ")" + \
|
|
407
|
-
") | snapend-manifest(" + \
|
|
408
|
-
", ".join(SNAPEND_MANIFEST_TYPES.keys()) + ")"
|
|
409
398
|
snapctl_error(
|
|
410
|
-
message=
|
|
399
|
+
message="Invalid Download format.", code=SNAPCTL_INPUT_ERROR)
|
|
411
400
|
# Check the Protos category
|
|
412
401
|
if self.category == 'protos' and self.category_type not in Snapend.CATEGORY_TYPE_PROTOS:
|
|
413
402
|
snapctl_error(
|
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': 'false',
|
|
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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## beta-0.50.0
|
|
2
|
+
##### June 5, 2025
|
|
3
|
+
|
|
4
|
+
### Bug fix
|
|
5
|
+
- Success, info or warning messages were sometimes getting truncated in the CLI output. This has been fixed.
|
|
6
|
+
- Snapctl byosnap and byogs now support features that could only work with Buildkit.
|
|
7
|
+
|
|
8
|
+
### Feature
|
|
9
|
+
- The `snapctl byosnap upload-docs` command now supports `--version` to keep it consistent with all the new commands like `sync` and `publish`.
|
|
10
|
+
|
snapctl/main.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
"""
|
|
4
4
|
import configparser
|
|
5
5
|
import os
|
|
6
|
-
import json
|
|
7
6
|
from sys import platform
|
|
8
7
|
from typing import Union
|
|
9
8
|
import typer
|
|
@@ -24,6 +23,7 @@ from snapctl.config.hashes import PROTOS_TYPES, SERVICE_IDS, \
|
|
|
24
23
|
SNAPEND_MANIFEST_TYPES, SDK_TYPES
|
|
25
24
|
from snapctl.utils.echo import error, success, info
|
|
26
25
|
from snapctl.utils.helper import validate_api_key
|
|
26
|
+
from snapctl.utils.telemetry import telemetry
|
|
27
27
|
|
|
28
28
|
######### Globals #########
|
|
29
29
|
|
|
@@ -91,6 +91,21 @@ def extract_config(extract_key: str, profile: Union[str, None] = None) -> object
|
|
|
91
91
|
return result
|
|
92
92
|
|
|
93
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
|
+
|
|
94
109
|
def get_base_url(api_key: Union[str, None]) -> str:
|
|
95
110
|
"""
|
|
96
111
|
Returns the base url based on the api_key
|
|
@@ -154,6 +169,7 @@ def default_context_callback(ctx: typer.Context):
|
|
|
154
169
|
ctx.obj['api_key'] = api_key_obj['value']
|
|
155
170
|
ctx.obj['api_key_location'] = api_key_obj['location']
|
|
156
171
|
ctx.obj['profile'] = DEFAULT_PROFILE
|
|
172
|
+
ctx.obj['environment'] = get_environment(api_key_obj['value'])
|
|
157
173
|
ctx.obj['base_url'] = get_base_url(api_key_obj['value'])
|
|
158
174
|
ctx.obj['base_snapend_url'] = get_base_snapend_url(api_key_obj['value'])
|
|
159
175
|
|
|
@@ -174,6 +190,7 @@ def api_key_context_callback(
|
|
|
174
190
|
ctx.obj['version'] = VERSION
|
|
175
191
|
ctx.obj['api_key'] = api_key
|
|
176
192
|
ctx.obj['api_key_location'] = 'command-line-argument'
|
|
193
|
+
ctx.obj['environment'] = get_environment(api_key)
|
|
177
194
|
ctx.obj['base_url'] = get_base_url(api_key)
|
|
178
195
|
|
|
179
196
|
|
|
@@ -206,6 +223,7 @@ def profile_context_callback(
|
|
|
206
223
|
ctx.obj['api_key'] = api_key_obj['value']
|
|
207
224
|
ctx.obj['api_key_location'] = api_key_obj['location']
|
|
208
225
|
ctx.obj['profile'] = profile if profile else DEFAULT_PROFILE
|
|
226
|
+
ctx.obj['environment'] = get_environment(api_key_obj['value'])
|
|
209
227
|
ctx.obj['base_url'] = get_base_url(api_key_obj['value'])
|
|
210
228
|
|
|
211
229
|
|
|
@@ -237,6 +255,7 @@ def common(
|
|
|
237
255
|
|
|
238
256
|
|
|
239
257
|
@app.command()
|
|
258
|
+
@telemetry("validate", subcommand_arg="subcommand")
|
|
240
259
|
def validate(
|
|
241
260
|
ctx: typer.Context,
|
|
242
261
|
api_key: Union[str, None] = typer.Option(
|
|
@@ -256,6 +275,7 @@ def validate(
|
|
|
256
275
|
|
|
257
276
|
|
|
258
277
|
@app.command()
|
|
278
|
+
@telemetry("release_notes", subcommand_arg="subcommand")
|
|
259
279
|
def release_notes(
|
|
260
280
|
ctx: typer.Context,
|
|
261
281
|
subcommand: str = typer.Argument(
|
|
@@ -284,6 +304,7 @@ def release_notes(
|
|
|
284
304
|
|
|
285
305
|
|
|
286
306
|
@app.command()
|
|
307
|
+
@telemetry("byogs", subcommand_arg="subcommand")
|
|
287
308
|
def byogs(
|
|
288
309
|
ctx: typer.Context,
|
|
289
310
|
# Required fields
|
|
@@ -353,6 +374,7 @@ def byogs(
|
|
|
353
374
|
|
|
354
375
|
|
|
355
376
|
@app.command()
|
|
377
|
+
@telemetry("byosnap", subcommand_arg="subcommand")
|
|
356
378
|
def byosnap(
|
|
357
379
|
ctx: typer.Context,
|
|
358
380
|
# Required fields
|
|
@@ -403,7 +425,7 @@ def byosnap(
|
|
|
403
425
|
help="(req: create) Language of your snap - " +
|
|
404
426
|
", ".join(ByoSnap.LANGUAGES) + "."
|
|
405
427
|
),
|
|
406
|
-
# publish-image, publish-version, publish, sync
|
|
428
|
+
# publish-image, publish-version, publish, sync, upload-docs
|
|
407
429
|
tag: str = typer.Option(
|
|
408
430
|
None, "--tag", help=(
|
|
409
431
|
"(req: publish-image, publish-version, upload-docs) (optional: publish, sync) Tag for your snap"
|
|
@@ -461,6 +483,7 @@ def byosnap(
|
|
|
461
483
|
|
|
462
484
|
|
|
463
485
|
@app.command()
|
|
486
|
+
@telemetry("game", subcommand_arg="subcommand")
|
|
464
487
|
def game(
|
|
465
488
|
ctx: typer.Context,
|
|
466
489
|
# Required fields
|
|
@@ -496,6 +519,7 @@ def game(
|
|
|
496
519
|
|
|
497
520
|
|
|
498
521
|
@app.command()
|
|
522
|
+
@telemetry("generate", subcommand_arg="subcommand")
|
|
499
523
|
def generate(
|
|
500
524
|
ctx: typer.Context,
|
|
501
525
|
# Required fields
|
|
@@ -542,6 +566,7 @@ def generate(
|
|
|
542
566
|
|
|
543
567
|
|
|
544
568
|
@app.command()
|
|
569
|
+
@telemetry("snapend", subcommand_arg="subcommand")
|
|
545
570
|
def snapend(
|
|
546
571
|
ctx: typer.Context,
|
|
547
572
|
# Required fields
|
|
@@ -685,6 +710,7 @@ def snapend(
|
|
|
685
710
|
|
|
686
711
|
|
|
687
712
|
@app.command()
|
|
713
|
+
@telemetry("byows", subcommand_arg="subcommand")
|
|
688
714
|
def byows(
|
|
689
715
|
ctx: typer.Context,
|
|
690
716
|
# Required fields
|
snapctl/utils/echo.py
CHANGED
|
@@ -4,38 +4,49 @@ This module contains functions to print messages to the console.
|
|
|
4
4
|
import json
|
|
5
5
|
import typer
|
|
6
6
|
from rich import print
|
|
7
|
+
from rich.markup import escape
|
|
7
8
|
from snapctl.config.constants import SNAPCTL_ERROR
|
|
8
9
|
from snapctl.types.definitions import ErrorResponse
|
|
9
10
|
# Run `python -m rich.emoji` to get a list of all emojis that are supported
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def
|
|
13
|
+
def _stringify_message(msg: object) -> str:
|
|
14
|
+
if isinstance(msg, (dict, list)):
|
|
15
|
+
return json.dumps(msg, indent=2)
|
|
16
|
+
return str(msg)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def error(msg: object, code: int = SNAPCTL_ERROR, data: object = None) -> None:
|
|
13
20
|
"""
|
|
14
21
|
Prints an error message to the console.
|
|
15
22
|
"""
|
|
23
|
+
msg = _stringify_message(msg)
|
|
16
24
|
error_response = ErrorResponse(
|
|
17
25
|
error=True, code=code, msg=msg, data=data if data else ''
|
|
18
26
|
)
|
|
19
|
-
print(f"[bold red]Error[/bold red] {msg}")
|
|
27
|
+
print(f"[bold red]Error[/bold red] {escape(msg)}")
|
|
20
28
|
typer.echo(json.dumps(error_response.to_dict()), err=True)
|
|
21
29
|
|
|
22
30
|
|
|
23
|
-
def warning(msg:
|
|
31
|
+
def warning(msg: object) -> None:
|
|
24
32
|
"""
|
|
25
33
|
Prints a warning message to the console.
|
|
26
34
|
"""
|
|
27
|
-
|
|
35
|
+
msg = _stringify_message(msg)
|
|
36
|
+
print(f"[bold yellow]Warning[/bold yellow] {escape(msg)}")
|
|
28
37
|
|
|
29
38
|
|
|
30
|
-
def info(msg:
|
|
39
|
+
def info(msg: object) -> None:
|
|
31
40
|
"""
|
|
32
41
|
Prints an info message to the console.
|
|
33
42
|
"""
|
|
34
|
-
|
|
43
|
+
msg = _stringify_message(msg)
|
|
44
|
+
print(f"[bold blue]Info[/bold blue] {escape(msg)}")
|
|
35
45
|
|
|
36
46
|
|
|
37
|
-
def success(msg:
|
|
47
|
+
def success(msg: object) -> None:
|
|
38
48
|
"""
|
|
39
49
|
Prints a success message to the console.
|
|
40
50
|
"""
|
|
41
|
-
|
|
51
|
+
msg = _stringify_message(msg)
|
|
52
|
+
print(f"[bold green]Success[/bold green] {escape(msg)}")
|
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=
|
|
5
|
-
snapctl/commands/byosnap.py,sha256=
|
|
6
|
-
snapctl/commands/byows.py,sha256=
|
|
4
|
+
snapctl/commands/byogs.py,sha256=fwXM7X_IqkVPAByGWWxwNzJeZlDTsh_ApWNH3IhyVVM,20076
|
|
5
|
+
snapctl/commands/byosnap.py,sha256=TAITdbB_FhGk7K94bPDDjykXbU9XwO9opW__qdeg0SQ,78187
|
|
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=-Z4GjVvJ39H9XCCTx6SUlAUWOwam5Nc0V6JdbR2IY4c,39675
|
|
11
11
|
snapctl/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
snapctl/config/
|
|
12
|
+
snapctl/config/app.py,sha256=QXSy10Yrw9yhV7VlAwxxdUCRrphoP9-2-sEaaL2SZa4,889
|
|
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
|
|
@@ -28,16 +29,18 @@ snapctl/data/releases/beta-0.49.0.mdx,sha256=pqJP7NfM3ZMxs4SoeANCdDqTxjR9IBGbBI3
|
|
|
28
29
|
snapctl/data/releases/beta-0.49.1.mdx,sha256=4EXupEegYffnL5bu_XWxGbzIPp9xOzl7t4xbmhq52fU,200
|
|
29
30
|
snapctl/data/releases/beta-0.49.2.mdx,sha256=Vv_OIQyPieEGI9FgTUT0v5Gm0PC2JWa2FqcCUhnvx6s,176
|
|
30
31
|
snapctl/data/releases/beta-0.49.3.mdx,sha256=XmSfpiH-LqvKS2REJ5FGDcLgyEN-OcSzwrXNI_EhiGc,234
|
|
31
|
-
snapctl/data/releases/beta-0.
|
|
32
|
-
snapctl/data/releases/beta-0.
|
|
33
|
-
snapctl/
|
|
32
|
+
snapctl/data/releases/beta-0.50.0.mdx,sha256=_w9f1HjHqukQ8IaTfUdk2W3gBcSwjMyMA8-rjuUxYTo,399
|
|
33
|
+
snapctl/data/releases/beta-0.51.0.mdx,sha256=5_xY29NJtH3Qa92UycCudrwDkSlUHRVNnP4GE4hF7-Y,93
|
|
34
|
+
snapctl/data/releases/beta-0.53.1.mdx,sha256=b__2LPkDhU3cTw5UC19Hxv1rDz6BsSLeI6fhHjOCP08,149
|
|
35
|
+
snapctl/main.py,sha256=BdVjXyCX64d3JIhPjDo4QEGhos6RAcQqSdY3MHBQNc4,26264
|
|
34
36
|
snapctl/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
37
|
snapctl/types/definitions.py,sha256=EQzLeiXkJ8ISRlCqHMviNVsWWpmhWjpKaOBLdlvOTmY,644
|
|
36
38
|
snapctl/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
-
snapctl/utils/echo.py,sha256=
|
|
38
|
-
snapctl/utils/helper.py,sha256=
|
|
39
|
-
snapctl
|
|
40
|
-
snapctl-0.
|
|
41
|
-
snapctl-0.
|
|
42
|
-
snapctl-0.
|
|
43
|
-
snapctl-0.
|
|
39
|
+
snapctl/utils/echo.py,sha256=9fxrPW9kNJwiJvJBFy9RlAP4qGRcPTOHfQb3pQh3CMg,1446
|
|
40
|
+
snapctl/utils/helper.py,sha256=5c5jfAYD4WsvPpuCLMCz3tPvji28786_pD9TZkRSFIs,7179
|
|
41
|
+
snapctl/utils/telemetry.py,sha256=_VntKXUrKgJoLKCskH0Z5VhthiVo8CP572U0mcZIH4k,5028
|
|
42
|
+
snapctl-0.53.1.dist-info/LICENSE,sha256=6AcXm54KFSpmUI1ji9NIBd4Xl-DtjTqiyjBzfVb_CEk,2804
|
|
43
|
+
snapctl-0.53.1.dist-info/METADATA,sha256=vns-UuApeJGMXSEk12QjrBTaGHacAM6FcukJS3-Nxds,36275
|
|
44
|
+
snapctl-0.53.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
45
|
+
snapctl-0.53.1.dist-info/entry_points.txt,sha256=tkKW9MzmFdRs6Bgkv29G78i9WEBK4WIOWunPfe3t2Wg,44
|
|
46
|
+
snapctl-0.53.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|