snapctl 1.0.3__py3-none-any.whl → 1.1.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.

@@ -0,0 +1,541 @@
1
+ """
2
+ Snapend manifest CLI commands
3
+ """
4
+ import os
5
+ from typing import Union
6
+ import json
7
+ from requests.exceptions import RequestException
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+ from snapctl.config.constants import SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, SNAPCTL_INPUT_ERROR, \
10
+ SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, \
11
+ SNAPCTL_INTERNAL_SERVER_ERROR
12
+ from snapctl.commands.snaps import Snaps
13
+ from snapctl.utils.helper import snapctl_error, snapctl_success
14
+ from snapctl.utils.echo import info, warning, success
15
+
16
+
17
+ class SnapendManifest:
18
+ """
19
+ CLI commands exposed for Snapend manifest
20
+ """
21
+ SUBCOMMANDS = ['create', 'update', 'upgrade']
22
+ ENVIRONMENTS = ['DEVELOPMENT', 'STAGING', 'PRODUCTION']
23
+ FEATURES = ['WEB_SOCKETS']
24
+ AUTH_SNAP_ID = 'auth'
25
+
26
+ def __init__(
27
+ self, *, subcommand: str, base_url: str, api_key: Union[str, None],
28
+ name: str = 'my-snapend',
29
+ environment: str = 'DEVELOPMENT',
30
+ manifest_path_filename: Union[str, None] = None,
31
+ snaps: Union[str, None] = None,
32
+ features: Union[str, None] = None,
33
+ out_path_filename: Union[str, None] = None,
34
+ ) -> None:
35
+ self.subcommand: str = subcommand
36
+ self.base_url: str = base_url
37
+ self.api_key: Union[str, None] = api_key
38
+ self.name: str = name
39
+ self.environment: str = environment
40
+ self.manifest_path_filename: Union[str, None] = manifest_path_filename
41
+ self.manifest: Union[dict, None] = None
42
+ self.out_path_filename: Union[str, None] = out_path_filename
43
+ self.snaps = snaps
44
+ self.features = features
45
+ self.remote_snaps: list = self.load_snaps()
46
+ # Setup
47
+ self.setup_manifest()
48
+ # Validate input
49
+ self.validate_input()
50
+
51
+ def setup_manifest(self) -> bool:
52
+ """
53
+ Read a manifest (JSON or YAML) and saves it
54
+ Supports extensions: .json, .yaml, .yml
55
+ If the extension is unknown, tries JSON then YAML.
56
+ """
57
+ def parse_json(s: str):
58
+ return json.loads(s)
59
+
60
+ def parse_yaml(s: str):
61
+ try:
62
+ import yaml # type: ignore
63
+ except ImportError as e:
64
+ raise RuntimeError(
65
+ "YAML file provided but PyYAML is not installed. "
66
+ "Install with: pip install pyyaml"
67
+ ) from e
68
+ return yaml.safe_load(s)
69
+
70
+ if not self.manifest_path_filename:
71
+ return False
72
+ with open(self.manifest_path_filename, "r", encoding="utf-8") as f:
73
+ text = f.read()
74
+
75
+ ext = os.path.splitext(self.manifest_path_filename)[1].lower()
76
+ if ext == ".json":
77
+ parsers = (parse_json, parse_yaml)
78
+ elif ext in (".yaml", ".yml"):
79
+ parsers = (parse_yaml, parse_json)
80
+ else:
81
+ parsers = (parse_json, parse_yaml)
82
+
83
+ last_err = None
84
+ data = None
85
+ for parser in parsers:
86
+ try:
87
+ data = parser(text)
88
+ break
89
+ except Exception as e:
90
+ last_err = e
91
+
92
+ if data is None:
93
+ return False
94
+ if not isinstance(data, dict):
95
+ return False
96
+
97
+ try:
98
+ self.manifest = data
99
+ except KeyError as e:
100
+ pass
101
+ return False
102
+
103
+ def load_snaps(self) -> list:
104
+ """
105
+ Load snaps from the Snapser portal
106
+ """
107
+ snaps_response = Snaps.get_snaps(self.base_url, self.api_key)
108
+ if 'services' in snaps_response:
109
+ return snaps_response['services']
110
+ return []
111
+
112
+ def validate_input(self) -> None:
113
+ """
114
+ Validator
115
+ """
116
+ # Check API Key and Base URL
117
+ if not self.api_key or self.base_url == '':
118
+ snapctl_error(
119
+ message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
120
+ # Check subcommand
121
+ if not self.subcommand in SnapendManifest.SUBCOMMANDS:
122
+ snapctl_error(
123
+ message="Invalid command. Valid commands are " +
124
+ f"{', '.join(SnapendManifest.SUBCOMMANDS)}.",
125
+ code=SNAPCTL_INPUT_ERROR)
126
+ if len(self.remote_snaps) == 0:
127
+ snapctl_error(
128
+ message="Something went wrong. No snaps found. Please try again in some time.",
129
+ code=SNAPCTL_INTERNAL_SERVER_ERROR)
130
+ if self.subcommand == 'create':
131
+ if not self.name or not self.environment:
132
+ snapctl_error(
133
+ message="Name and environment are required for create command.",
134
+ code=SNAPCTL_INPUT_ERROR)
135
+ if self.environment not in SnapendManifest.ENVIRONMENTS:
136
+ snapctl_error(
137
+ message="Environment must be one of " +
138
+ f"{', '.join(SnapendManifest.ENVIRONMENTS)}.",
139
+ code=SNAPCTL_INPUT_ERROR)
140
+ if not self.out_path_filename:
141
+ snapctl_error(
142
+ message="Output path is required for create command.",
143
+ code=SNAPCTL_INPUT_ERROR)
144
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
145
+ self.out_path_filename.endswith('.yaml') or
146
+ self.out_path_filename.endswith('.yml')):
147
+ snapctl_error(
148
+ message="Output path must end with .json, .yaml or .yml",
149
+ code=SNAPCTL_INPUT_ERROR)
150
+ if not self.snaps or self.snaps == '':
151
+ snapctl_error(
152
+ message="At least one snap ID is required to create a " +
153
+ "snapend manifest.",
154
+ code=SNAPCTL_INPUT_ERROR)
155
+ if self.features:
156
+ for feature in self.features.split(','):
157
+ feature = feature.strip()
158
+ if feature.upper() not in SnapendManifest.FEATURES:
159
+ snapctl_error(
160
+ message="-add-features must be one of " +
161
+ f"{', '.join(SnapendManifest.FEATURES)}.",
162
+ code=SNAPCTL_INPUT_ERROR)
163
+ elif self.subcommand == 'update':
164
+ if not self.manifest_path_filename:
165
+ snapctl_error(
166
+ message="Manifest path is required for update command.",
167
+ code=SNAPCTL_INPUT_ERROR)
168
+ if (not self.snaps or self.snaps == '') and \
169
+ (not self.features or self.features == ''):
170
+ snapctl_error(
171
+ message="At least one of snaps or features " +
172
+ "is required to update a snapend manifest.",
173
+ code=SNAPCTL_INPUT_ERROR)
174
+ if not self.out_path_filename:
175
+ snapctl_error(
176
+ message="Output path is required for update command.",
177
+ code=SNAPCTL_INPUT_ERROR)
178
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
179
+ self.out_path_filename.endswith('.yaml') or
180
+ self.out_path_filename.endswith('.yml')):
181
+ snapctl_error(
182
+ message="Output path must end with .json, .yaml or .yml",
183
+ code=SNAPCTL_INPUT_ERROR)
184
+ if not self.manifest:
185
+ snapctl_error(
186
+ message="Unable to read the manifest file. " +
187
+ "Please check the file and try again.",
188
+ code=SNAPCTL_INPUT_ERROR)
189
+ if 'service_definitions' not in self.manifest:
190
+ snapctl_error(
191
+ message="Invalid manifest file. Need service_definitions. " +
192
+ "Please check the file and try again.",
193
+ code=SNAPCTL_INPUT_ERROR)
194
+ elif self.subcommand == 'upgrade':
195
+ if not self.manifest_path_filename:
196
+ snapctl_error(
197
+ message="Manifest path is required for update command.",
198
+ code=SNAPCTL_INPUT_ERROR)
199
+ if not self.out_path_filename:
200
+ snapctl_error(
201
+ message="Output path is required for update command.",
202
+ code=SNAPCTL_INPUT_ERROR)
203
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
204
+ self.out_path_filename.endswith('.yaml') or
205
+ self.out_path_filename.endswith('.yml')):
206
+ snapctl_error(
207
+ message="Output path must end with .json, .yaml or .yml",
208
+ code=SNAPCTL_INPUT_ERROR)
209
+ if not self.manifest:
210
+ snapctl_error(
211
+ message="Unable to read the manifest file. " +
212
+ "Please check the file and try again.",
213
+ code=SNAPCTL_INPUT_ERROR)
214
+ if 'service_definitions' not in self.manifest:
215
+ snapctl_error(
216
+ message="Invalid manifest file. Need service_definitions. " +
217
+ "Please check the file and try again.",
218
+ code=SNAPCTL_INPUT_ERROR)
219
+ elif self.subcommand == 'validate':
220
+ if not self.manifest_path_filename:
221
+ snapctl_error(
222
+ message="Manifest path is required for validate command.",
223
+ code=SNAPCTL_INPUT_ERROR)
224
+
225
+ def _get_snap_sd(self, snap_id) -> dict:
226
+ """
227
+ Get snap service definition
228
+ """
229
+ for snap in self.remote_snaps:
230
+ if snap['id'] == snap_id:
231
+ snap_sd = {
232
+ "id": snap['id'],
233
+ "language": snap['language'],
234
+ "version": snap['latest_version'],
235
+ "author_id": snap['author_id'],
236
+ "category": snap['category'],
237
+ "subcategory": snap['subcategory'],
238
+ "data_dependencies": [],
239
+ }
240
+ for versions in snap['versions']:
241
+ if versions['version'] == snap['latest_version']:
242
+ snap_sd['data_dependencies'] = \
243
+ versions['data_dependencies']
244
+ return snap_sd
245
+ raise ValueError(
246
+ f"Snap service definition with id '{snap_id}' not found")
247
+
248
+ # Commands
249
+ def create(self) -> bool:
250
+ """
251
+ Create a snapend manifest
252
+ @test -
253
+ `python -m snapctl snapend-manifest create --name my-dev-snapend --env DEVELOPMENT --snaps auth,analytics --out-path-filename ./snapend-manifest.json`
254
+ """
255
+ progress = Progress(
256
+ SpinnerColumn(),
257
+ TextColumn("[progress.description]{task.description}"),
258
+ transient=True,
259
+ )
260
+ progress.start()
261
+ progress.add_task(
262
+ description='Enumerating all your games...', total=None)
263
+ try:
264
+ new_manifest = {
265
+ "version": "v1",
266
+ "name": self.name,
267
+ "environment": self.environment,
268
+ "service_definitions": [],
269
+ "feature_definitions": [],
270
+ "external_endpoints": [],
271
+ "settings": []
272
+ }
273
+ snap_ids = [snap_id.strip()
274
+ for snap_id in self.snaps.split(',')]
275
+ for snap_id in snap_ids:
276
+ snap_found = False
277
+ for snap in self.remote_snaps:
278
+ if snap['id'] == snap_id:
279
+ snap_sd = self._get_snap_sd(snap_id)
280
+ new_manifest['service_definitions'].append(snap_sd)
281
+ snap_found = True
282
+ break
283
+ if not snap_found:
284
+ snapctl_error(
285
+ message=f"Snap ID {snap_id} not found in your snaps.",
286
+ code=SNAPCTL_INPUT_ERROR,
287
+ progress=progress)
288
+ found_auth = False
289
+ for final_snap in new_manifest['service_definitions']:
290
+ if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
291
+ found_auth = True
292
+ break
293
+ if not found_auth:
294
+ auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
295
+ new_manifest['service_definitions'].append(auth_sd)
296
+ warning(
297
+ 'Auth snap is required for snapend. Added auth snap to the manifest.')
298
+ new_manifest['service_definitions'].sort(key=lambda x: x["id"])
299
+ if self.features and self.features != '':
300
+ features = [feature.strip()
301
+ for feature in self.features.split(',')]
302
+ for feature in features:
303
+ if feature.upper() not in new_manifest['feature_definitions']:
304
+ new_manifest['feature_definitions'].append(
305
+ feature.upper())
306
+ if self.out_path_filename:
307
+ # Based on the out-path extension, write JSON or YAML
308
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
309
+ try:
310
+ import yaml # type: ignore
311
+ except ImportError as e:
312
+ snapctl_error(
313
+ message="YAML output requested but PyYAML is not installed. "
314
+ "Install with: pip install pyyaml",
315
+ code=SNAPCTL_INPUT_ERROR,
316
+ progress=progress)
317
+ with open(self.out_path_filename, 'w') as out_file:
318
+ yaml.dump(new_manifest, out_file, sort_keys=False)
319
+ else:
320
+ with open(self.out_path_filename, 'w') as out_file:
321
+ out_file.write(json.dumps(new_manifest, indent=4))
322
+ info(f"Output written to {self.out_path_filename}")
323
+ success("You can now use this manifest to create a snapend " +
324
+ "environment using the command 'snapend create " +
325
+ "--manifest-path-filename $fullPathToManifest --application-id $appId --blocking'")
326
+ snapctl_success(
327
+ message="Snapend manifest created successfully.",
328
+ progress=progress)
329
+ else:
330
+ snapctl_success(
331
+ message=new_manifest, progress=progress)
332
+ except ValueError as e:
333
+ snapctl_error(
334
+ message=f"Exception: {e}",
335
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
336
+ except RequestException as e:
337
+ snapctl_error(
338
+ message=f"Exception: Unable to create snapend manifest {e}",
339
+ code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
340
+ finally:
341
+ progress.stop()
342
+ snapctl_error(
343
+ message='Failed to create snapend manifest.',
344
+ code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
345
+
346
+ def update(self) -> bool:
347
+ """
348
+ Update a snapend manifest
349
+ @test -
350
+ `python -m snapctl snapend-manifest update --manifest-path-filename ./snapend-manifest.json --features WEB_SOCKETS --out-path-filename ./snapend-updated-manifest.json`
351
+ """
352
+ progress = Progress(
353
+ SpinnerColumn(),
354
+ TextColumn("[progress.description]{task.description}"),
355
+ transient=True,
356
+ )
357
+ progress.start()
358
+ progress.add_task(
359
+ description='Updating snapend manifest...', total=None)
360
+ try:
361
+ if 'applied_configuration' in self.manifest:
362
+ info('Applied configuration found in the manifest. ')
363
+ warning(
364
+ 'You need to ensure you have synced the manifest from remote. ' +
365
+ 'Else if you try applying the newly generated manifest it may not work.')
366
+
367
+ current_snaps = self.manifest['service_definitions']
368
+ current_snap_ids = [snap['id'] for snap in current_snaps]
369
+ final_snaps = []
370
+ if self.snaps and self.snaps != '':
371
+ for snap_id in self.snaps.split(','):
372
+ snap_id = snap_id.strip()
373
+ if snap_id == '':
374
+ continue
375
+ if snap_id in current_snap_ids:
376
+ warning(
377
+ f"Snap {snap_id} already exists in the manifest. Skipping...")
378
+ final_snaps.append(
379
+ current_snaps[current_snap_ids.index(snap_id)])
380
+ continue
381
+ snap_found = False
382
+ for snap in self.remote_snaps:
383
+ if snap['id'] == snap_id:
384
+ snap_found = True
385
+ snap_sd = self._get_snap_sd(snap_id)
386
+ final_snaps.append(snap_sd)
387
+ info(f"Added snap {snap_id} to the manifest.")
388
+ break
389
+ if not snap_found:
390
+ snapctl_error(
391
+ message=f"Snap ID {snap_id} not found in your snaps.",
392
+ code=SNAPCTL_INPUT_ERROR,
393
+ progress=progress)
394
+ found_auth = False
395
+ for final_snap in final_snaps:
396
+ if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
397
+ found_auth = True
398
+ break
399
+ if not found_auth:
400
+ auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
401
+ final_snaps.append(auth_sd)
402
+ warning(
403
+ 'Auth snap is required for snapend. Added auth snap to the manifest.')
404
+ warning(
405
+ f'Old snaps list "{",".join(current_snap_ids)}" will be ' +
406
+ f'replaced with new snaps list "{",".join([snap['id'] for snap in final_snaps])}"')
407
+ self.manifest['service_definitions'] = final_snaps
408
+
409
+ final_features = []
410
+ if self.features and self.features != '':
411
+ current_features = self.manifest['feature_definitions']
412
+ for feature in self.features.split(','):
413
+ feature = feature.strip()
414
+ if feature == '':
415
+ continue
416
+ if feature.upper() in current_features:
417
+ warning(
418
+ f"Feature {feature} already exists in the manifest. Skipping...")
419
+ final_features.append(feature.upper())
420
+ warning(
421
+ f'Old features list: "{",".join(self.manifest["feature_definitions"])}" will ' +
422
+ f'be replaced with new features list: "{",".join([feature for feature in final_features])}"')
423
+ final_features.sort()
424
+ self.manifest['feature_definitions'] = final_features
425
+
426
+ # Write output
427
+ # Based on the out-path extension, write JSON or YAML
428
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
429
+ try:
430
+ import yaml # type: ignore
431
+ except ImportError as e:
432
+ snapctl_error(
433
+ message="YAML output requested but PyYAML is not installed. "
434
+ "Install with: pip install pyyaml",
435
+ code=SNAPCTL_INPUT_ERROR,
436
+ progress=progress)
437
+ with open(self.out_path_filename, 'w') as out_file:
438
+ yaml.dump(self.manifest, out_file, sort_keys=False)
439
+ else:
440
+ with open(self.out_path_filename, 'w') as out_file:
441
+ out_file.write(json.dumps(self.manifest, indent=4))
442
+ info(f"Output written to {self.out_path_filename}")
443
+ snapctl_success(
444
+ message="Snapend manifest updated successfully.",
445
+ progress=progress)
446
+ except ValueError as e:
447
+ snapctl_error(
448
+ message=f"Exception: {e}",
449
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
450
+ except RequestException as e:
451
+ snapctl_error(
452
+ message=f"Exception: Unable to update snapend manifest {e}",
453
+ code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)
454
+ finally:
455
+ progress.stop()
456
+ snapctl_error(
457
+ message='Failed to update the snapend manifest.',
458
+ code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)
459
+
460
+ def upgrade(self) -> bool:
461
+ """
462
+ Upgrade all Snap versions to the latest in a snapend manifest
463
+ @test -
464
+ `python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --snaps auth,analytics --out-path-filename ./snapend-upgraded-manifest.json`
465
+ `python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --out-path-filename ./snapend-upgraded-manifest.json`
466
+ """
467
+ progress = Progress(
468
+ SpinnerColumn(),
469
+ TextColumn("[progress.description]{task.description}"),
470
+ transient=True,
471
+ )
472
+ progress.start()
473
+ progress.add_task(
474
+ description='Updating snapend manifest...', total=None)
475
+ try:
476
+ if 'applied_configuration' in self.manifest:
477
+ info('Applied configuration found in the manifest. ')
478
+ warning(
479
+ 'You need to ensure you have synced the manifest from remote. ' +
480
+ 'Else if you try applying the newly generated manifest it may not work.')
481
+
482
+ current_snaps = self.manifest['service_definitions']
483
+ force_snaps_upgrade = []
484
+ if self.snaps and self.snaps != '':
485
+ force_snaps_upgrade = [snap_id.strip()
486
+ for snap_id in self.snaps.split(',')]
487
+ # Look at self.remote_snaps, get the latest version for each snap
488
+ for i, snap in enumerate(current_snaps):
489
+ for remote_snap in self.remote_snaps:
490
+ if remote_snap['id'] == snap['id']:
491
+ if len(force_snaps_upgrade) > 0 and \
492
+ snap['id'] not in force_snaps_upgrade:
493
+ info(
494
+ f"Skipping snap {snap['id']} as it's not in the " +
495
+ f"--snaps list {','.join(force_snaps_upgrade)}")
496
+ break
497
+ if remote_snap['latest_version'] != snap['version']:
498
+ current_snaps[i] = self._get_snap_sd(snap['id'])
499
+ info(
500
+ f"Upgraded snap {snap['id']} from version " +
501
+ f"{snap['version']} to {remote_snap['latest_version']}.")
502
+ else:
503
+ info(
504
+ f"Snap {snap['id']} is already at the latest " +
505
+ f"version {snap['version']}. Skipping...")
506
+ break
507
+ self.manifest['service_definitions'] = current_snaps
508
+
509
+ # Write output
510
+ # Based on the out-path extension, write JSON or YAML
511
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
512
+ try:
513
+ import yaml # type: ignore
514
+ except ImportError as e:
515
+ snapctl_error(
516
+ message="YAML output requested but PyYAML is not installed. "
517
+ "Install with: pip install pyyaml",
518
+ code=SNAPCTL_INPUT_ERROR,
519
+ progress=progress)
520
+ with open(self.out_path_filename, 'w') as out_file:
521
+ yaml.dump(self.manifest, out_file, sort_keys=False)
522
+ else:
523
+ with open(self.out_path_filename, 'w') as out_file:
524
+ out_file.write(json.dumps(self.manifest, indent=4))
525
+ info(f"Output written to {self.out_path_filename}")
526
+ snapctl_success(
527
+ message="Snapend manifest upgraded successfully.",
528
+ progress=progress)
529
+ except ValueError as e:
530
+ snapctl_error(
531
+ message=f"Exception: {e}",
532
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
533
+ except RequestException as e:
534
+ snapctl_error(
535
+ message=f"Exception: Unable to upgrade the snapend manifest {e}",
536
+ code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
537
+ finally:
538
+ progress.stop()
539
+ snapctl_error(
540
+ message='Failed to upgrade the snapend manifest.',
541
+ code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
@@ -0,0 +1,109 @@
1
+ """
2
+ Snaps CLI commands
3
+ """
4
+ import json
5
+ from typing import Union
6
+ import requests
7
+ from requests.exceptions import RequestException
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+ from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
10
+ SNAPCTL_SNAPS_ENUMERATE_ERROR, SNAPCTL_INTERNAL_SERVER_ERROR
11
+ from snapctl.utils.helper import snapctl_error, snapctl_success
12
+ from snapctl.utils.echo import info
13
+
14
+
15
+ class Snaps:
16
+ """
17
+ CLI commands exposed for Snaps
18
+ """
19
+ SUBCOMMANDS = ['enumerate']
20
+
21
+ def __init__(
22
+ self, *, subcommand: str, base_url: str, api_key: Union[str, None],
23
+ out_path_filename: Union[str, None] = None
24
+ ) -> None:
25
+ self.subcommand: str = subcommand
26
+ self.base_url: str = base_url
27
+ self.api_key: Union[str, None] = api_key
28
+ self.out_path_filename: Union[str, None] = out_path_filename
29
+ # Validate input
30
+ self.validate_input()
31
+
32
+ def validate_input(self) -> None:
33
+ """
34
+ Validator
35
+ """
36
+ # Check API Key and Base URL
37
+ if not self.api_key or self.base_url == '':
38
+ snapctl_error(
39
+ message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
40
+ # Check subcommand
41
+ if not self.subcommand in Snaps.SUBCOMMANDS:
42
+ snapctl_error(
43
+ message="Invalid command. Valid commands are " +
44
+ f"{', '.join(Snaps.SUBCOMMANDS)}.",
45
+ code=SNAPCTL_INPUT_ERROR)
46
+ if self.subcommand == 'enumerate':
47
+ if self.out_path_filename:
48
+ if not (self.out_path_filename.endswith('.json')):
49
+ snapctl_error(
50
+ message="Output filename should end with .json",
51
+ code=SNAPCTL_INPUT_ERROR)
52
+ info(f"Output will be written to {self.out_path_filename}")
53
+
54
+ @staticmethod
55
+ def get_snaps(base_url: str, api_key: str) -> dict:
56
+ """
57
+ Get snaps
58
+ """
59
+ response_json = {}
60
+ try:
61
+ url = f"{base_url}/v1/snapser-api/services"
62
+ res = requests.get(
63
+ url, headers={'api-key': api_key},
64
+ timeout=SERVER_CALL_TIMEOUT
65
+ )
66
+ response_json = res.json()
67
+ except RequestException as e:
68
+ pass
69
+ return response_json
70
+
71
+ def enumerate(self) -> bool:
72
+ """
73
+ Enumerate all snaps
74
+ """
75
+ progress = Progress(
76
+ SpinnerColumn(),
77
+ TextColumn("[progress.description]{task.description}"),
78
+ transient=True,
79
+ )
80
+ progress.start()
81
+ progress.add_task(
82
+ description='Enumerating snaps...', total=None)
83
+ try:
84
+ response_json = Snaps.get_snaps(self.base_url, self.api_key)
85
+ if response_json == {}:
86
+ snapctl_error(
87
+ message="Something went wrong. No snaps found. Please try again in some time.",
88
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
89
+ if 'services' not in response_json:
90
+ snapctl_error(
91
+ message="Something went wrong. No snaps found. Please try again in some time.",
92
+ code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)
93
+ if self.out_path_filename:
94
+ with open(self.out_path_filename, 'w') as out_file:
95
+ out_file.write(json.dumps(response_json))
96
+ snapctl_success(
97
+ message=f"Output written to {self.out_path_filename}", progress=progress)
98
+ else:
99
+ snapctl_success(
100
+ message=response_json, progress=progress)
101
+ except RequestException as e:
102
+ snapctl_error(
103
+ message=f"Exception: Unable to enumerate snaps {e}",
104
+ code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)
105
+ finally:
106
+ progress.stop()
107
+ snapctl_error(
108
+ message='Failed to enumerate snaps.',
109
+ code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)