snapctl 0.22.3__py3-none-any.whl → 0.26.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 CHANGED
@@ -161,16 +161,18 @@ class ByoGs:
161
161
  ) as progress:
162
162
  progress.add_task(
163
163
  description='Building your snap...', total=None)
164
+ docker_file_path = f"{self.path}/{self.dockerfile}"
164
165
  if platform == "win32":
165
166
  response = subprocess.run([
166
167
  # f"docker build --no-cache -t {tag} {path}"
167
- 'docker', 'build', '--platform', build_platform, '-t', image_tag, self.path
168
+ 'docker', 'build', '--platform', build_platform, '-t', image_tag,
169
+ '-f', docker_file_path, self.path
168
170
  ], shell=True, check=False)
169
171
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
170
172
  else:
171
173
  response = subprocess.run([
172
174
  # f"docker build --no-cache -t {tag} {path}"
173
- f"docker build --platform {build_platform} -t {image_tag} {self.path}"
175
+ f"docker build --platform {build_platform} -t {image_tag} -f {docker_file_path} {self.path}"
174
176
  ], shell=True, check=False)
175
177
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
176
178
  if response.returncode:
@@ -169,16 +169,18 @@ class ByoSnap:
169
169
  ) as progress:
170
170
  progress.add_task(
171
171
  description='Building your snap...', total=None)
172
+ docker_file_path = f"{self.path}/{self.dockerfile}"
172
173
  if platform == "win32":
173
174
  response = subprocess.run([
174
175
  # f"docker build --no-cache -t {tag} {path}"
175
- 'docker', 'build', '--platform', build_platform, '-t', image_tag, self.path
176
+ 'docker', 'build', '--platform', build_platform, '-t', image_tag,
177
+ '-f', docker_file_path, self.path
176
178
  ], shell=True, check=False)
177
179
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
178
180
  else:
179
181
  response = subprocess.run([
180
182
  # f"docker build --no-cache -t {tag} {path}"
181
- f"docker build --platform {build_platform} -t {image_tag} {self.path}"
183
+ f"docker build --platform {build_platform} -t {image_tag} -f {docker_file_path} {self.path}"
182
184
  ], shell=True, check=False)
183
185
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
184
186
  if response.returncode:
@@ -0,0 +1,107 @@
1
+ """
2
+ Snapend CLI commands
3
+ """
4
+ import requests
5
+ from requests.exceptions import RequestException
6
+
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+ from snapctl.config.constants import SERVER_CALL_TIMEOUT
9
+ from snapctl.types.definitions import ResponseType
10
+ from snapctl.utils.echo import error, success
11
+
12
+
13
+ class Game:
14
+ """
15
+ CLI commands exposed for a Game
16
+ """
17
+ SUBCOMMANDS = ['create', 'enumerate']
18
+
19
+ def __init__(
20
+ self, subcommand: str, base_url: str, api_key: str | None, name: str | None
21
+ ) -> None:
22
+ self.subcommand: str = subcommand
23
+ self.base_url: str = base_url
24
+ self.api_key: str = api_key
25
+ self.name: str | None = name
26
+
27
+ def validate_input(self) -> ResponseType:
28
+ """
29
+ Validator
30
+ """
31
+ response: ResponseType = {
32
+ 'error': True,
33
+ 'msg': '',
34
+ 'data': []
35
+ }
36
+ # Check API Key and Base URL
37
+ if not self.api_key or self.base_url == '':
38
+ response['msg'] = "Missing API Key."
39
+ return response
40
+ # Check subcommand
41
+ if not self.subcommand in Game.SUBCOMMANDS:
42
+ response['msg'] = \
43
+ f"Invalid command. Valid commands are {', '.join(Game.SUBCOMMANDS)}."
44
+ return response
45
+ # Check sdk-download commands
46
+ if self.subcommand == 'create':
47
+ if self.name is None or self.name == '':
48
+ response['msg'] = "Missing game name."
49
+ return response
50
+ # Send success
51
+ response['error'] = False
52
+ return response
53
+
54
+ def create(self) -> bool:
55
+ """
56
+ Create a game
57
+ """
58
+ with Progress(
59
+ SpinnerColumn(),
60
+ TextColumn("[progress.description]{task.description}"),
61
+ transient=True,
62
+ ) as progress:
63
+ progress.add_task(
64
+ description='Creating a new game on Snapser...', total=None)
65
+ try:
66
+ url = f"{self.base_url}/v1/snapser-api/games"
67
+ payload = {
68
+ 'name': self.name
69
+ }
70
+ res = requests.post(
71
+ url, headers={'api-key': self.api_key},
72
+ json=payload, timeout=SERVER_CALL_TIMEOUT
73
+ )
74
+ if res.ok:
75
+ success(f"Game {self.name} has been created successfully.")
76
+ return True
77
+ error('Unable to create a new game. Reason: ' + res.text)
78
+ except RequestException as e:
79
+ error(f"Exception: Unable to download the SDK {e}")
80
+ return False
81
+
82
+ def enumerate(self) -> bool:
83
+ """
84
+ Enumerate all games
85
+ """
86
+ with Progress(
87
+ SpinnerColumn(),
88
+ TextColumn("[progress.description]{task.description}"),
89
+ transient=True,
90
+ ) as progress:
91
+ progress.add_task(
92
+ description='Enumerating all your games...', total=None)
93
+ try:
94
+ url = f"{self.base_url}/v1/snapser-api/games"
95
+ res = requests.get(
96
+ url, headers={'api-key': self.api_key},
97
+ timeout=SERVER_CALL_TIMEOUT
98
+ )
99
+ response_json = res.json()
100
+ if res.ok:
101
+ if 'games' in response_json:
102
+ success(response_json['games'])
103
+ return True
104
+ error(response_json)
105
+ except RequestException as e:
106
+ error(f"Exception: Unable to update your snapend {e}")
107
+ return False
@@ -5,13 +5,15 @@ from sys import platform
5
5
  from typing import Union
6
6
 
7
7
  import os
8
+ import json
8
9
  import time
9
10
  import requests
10
11
  from requests.exceptions import RequestException
11
12
 
12
13
  from rich.progress import Progress, SpinnerColumn, TextColumn
13
14
  from snapctl.config.constants import SERVER_CALL_TIMEOUT
14
- from snapctl.config.hashes import CLIENT_SDK_TYPES, SERVER_SDK_TYPES, PROTOS_TYPES
15
+ from snapctl.config.hashes import CLIENT_SDK_TYPES, SERVER_SDK_TYPES, PROTOS_TYPES, \
16
+ SNAPEND_MANIFEST_TYPES
15
17
  from snapctl.types.definitions import ResponseType
16
18
  from snapctl.utils.echo import error, success, info
17
19
 
@@ -20,38 +22,69 @@ class Snapend:
20
22
  """
21
23
  CLI commands exposed for a Snapend
22
24
  """
23
- SUBCOMMANDS = ['download', 'update', 'state']
25
+ SUBCOMMANDS = [
26
+ 'enumerate', 'clone', 'apply',
27
+ 'download', 'update', 'state'
28
+ ]
24
29
  DOWNLOAD_CATEGORY = [
25
- 'client-sdk', 'server-sdk', 'protos', 'admin-settings'
30
+ 'client-sdk', 'server-sdk', 'protos', 'admin-settings', 'snapend-manifest'
26
31
  ]
27
32
  DOWNLOAD_TYPE_NOT_REQUIRED = ['admin-settings']
28
33
  AUTH_TYPES = ['user', 'app']
34
+ ENV_TYPES = ['DEVELOPMENT', 'STAGING']
29
35
  BLOCKING_CALL_SLEEP = 5
30
- MAX_BLOCKING_RETRIES = 24
36
+ MAX_BLOCKING_RETRIES = 120
31
37
 
32
38
  def __init__(
33
- self, subcommand: str, base_url: str, api_key: str | None, snapend_id: str, category: str,
34
- platform_type: str, auth_type: str, path: Union[str, None], snaps: Union[str, None],
35
- byosnaps: Union[str, None], byogs: Union[str, None], blocking: bool = False
39
+ self, subcommand: str, base_url: str, api_key: str | None, snapend_id: str | None,
40
+ # Enumerate, Clone
41
+ game_id: str | None,
42
+ # Clone
43
+ name: str | None,
44
+ env: str | None,
45
+ # Clone, Apply, Promote
46
+ manifest_path: str | None,
47
+ # Download
48
+ category: str, platform_type: str, auth_type: str, snaps: str | None,
49
+ # Clone, Apply, Promote, Download
50
+ out_path: str | None,
51
+ # Update
52
+ byosnaps: str | None, byogs: str | None, blocking: bool = False
36
53
  ) -> None:
37
54
  self.subcommand: str = subcommand
38
55
  self.base_url: str = base_url
39
56
  self.api_key: str = api_key
40
57
  self.snapend_id: str = snapend_id
58
+ self.game_id: str | None = game_id
59
+ self.name: str = name
60
+ self.env: str = env
61
+ self.manifest_path: str | None = manifest_path
62
+ self.manifest_file_name: str | None = Snapend._get_manifest_file_name(
63
+ manifest_path
64
+ )
41
65
  self.category: str = category
42
66
  self.download_types: Union[
43
67
  dict[str, dict[str, str]], None
44
68
  ] = Snapend._make_download_type(category)
45
69
  self.auth_type: str = auth_type
46
70
  self.platform_type: str = platform_type
47
- self.path: Union[str, None] = path
48
- self.snaps: Union[str, None] = snaps
71
+ self.out_path: str | None = out_path
72
+ self.snaps: str | None = snaps
49
73
  self.byosnap_list: Union[list, None] = Snapend._make_byosnap_list(
50
74
  byosnaps) if byosnaps else None
51
- self.byogs_list: Union[str, None] = Snapend._make_byogs_list(
75
+ self.byogs_list: str | None = Snapend._make_byogs_list(
52
76
  byogs) if byogs else None
53
77
  self.blocking: bool = blocking
54
78
 
79
+ @staticmethod
80
+ def _get_manifest_file_name(manifest_path: str) -> str | None:
81
+ if manifest_path and manifest_path != '' and os.path.isfile(manifest_path):
82
+ file_name = os.path.basename(manifest_path)
83
+ if file_name.endswith('.json') or file_name.endswith('.yml') or \
84
+ file_name.endswith('.yaml'):
85
+ return file_name
86
+ return None
87
+
55
88
  @staticmethod
56
89
  def _make_download_type(category: str):
57
90
  if category == 'client-sdk':
@@ -60,6 +93,8 @@ class Snapend:
60
93
  return SERVER_SDK_TYPES
61
94
  if category == 'protos':
62
95
  return PROTOS_TYPES
96
+ if category == 'snapend-manifest':
97
+ return SNAPEND_MANIFEST_TYPES
63
98
  return None
64
99
 
65
100
  @staticmethod
@@ -112,16 +147,88 @@ class Snapend:
112
147
  error("Going past maximum tries. Exiting...")
113
148
  return False
114
149
  current_state = self._get_snapend_state()
115
- if current_state == 'INVALID':
116
- error("Unable to get the snapend state. Exiting...")
150
+ if current_state != 'IN_PROGRESS':
151
+ if current_state == 'LIVE':
152
+ success('Updated your snapend. Your snapend is Live.')
153
+ return True
154
+ error(
155
+ f"Update not completed successfully. Your Snapend status is {current_state}.")
117
156
  return False
118
- if current_state == 'LIVE':
119
- success('Updated your snapend. Your snapend is Live.')
120
- return True
121
157
  info(f'Current snapend state is {current_state}')
122
158
  info(f"Retrying in {Snapend.BLOCKING_CALL_SLEEP} seconds...")
123
159
  time.sleep(Snapend.BLOCKING_CALL_SLEEP)
124
160
 
161
+ def _assign_snapend_id(self, snapend_id: str) -> None:
162
+ self.snapend_id = snapend_id
163
+
164
+ def _setup_for_download(self, platform_type: str) -> bool:
165
+ '''
166
+ Called by subcommands that want to initiate a download of the new manifest post update
167
+ '''
168
+ download_category: str = 'snapend-manifest'
169
+ self.category = download_category
170
+ self.platform_type = platform_type
171
+ self.download_types: Union[
172
+ dict[str, dict[str, str]], None
173
+ ] = Snapend._make_download_type(download_category)
174
+
175
+ def _execute_download(self) -> bool:
176
+ try:
177
+ url = (
178
+ f"{self.base_url}/v1/snapser-api/snapends/{self.snapend_id}/"
179
+ f"download?category={self.category}"
180
+ )
181
+ if self.category not in Snapend.DOWNLOAD_TYPE_NOT_REQUIRED:
182
+ url += (
183
+ f"&type={self.download_types[self.platform_type]['type']}"
184
+ f"&subtype={self.download_types[self.platform_type]['subtype']}"
185
+ )
186
+ url_auth_type: str = 'user'
187
+ if self.category == 'server-sdk' and self.auth_type == 'app':
188
+ url_auth_type = 'app'
189
+ url += f"&auth_type={url_auth_type}"
190
+ if self.snaps:
191
+ url += f"&snaps={self.snaps}"
192
+ res = requests.get(
193
+ url, headers={'api-key': self.api_key}, timeout=SERVER_CALL_TIMEOUT
194
+ )
195
+ fn: str = ''
196
+ if self.category == 'admin-settings':
197
+ fn = f"snapser-{self.snapend_id}-admin-settings.json"
198
+ elif self.category == 'snapend-manifest':
199
+ fn = (
200
+ f"snapser-{self.snapend_id}-"
201
+ f"manifest.{self.download_types[self.platform_type]['type']}"
202
+ )
203
+ else:
204
+ fn = (
205
+ f"snapser-{self.snapend_id}-{self.category}"
206
+ f"-{self.platform_type}-{self.auth_type}.zip"
207
+ )
208
+ file_path_symbol = '/'
209
+ if platform == 'win32':
210
+ file_path_symbol = '\\'
211
+ if self.out_path is not None:
212
+ file_save_path = f"{self.out_path}{file_path_symbol}{fn}"
213
+ else:
214
+ file_save_path = f"{os.getcwd()}{file_path_symbol}{fn}"
215
+ if res.ok:
216
+ content: bytes = res.content
217
+ with open(file_save_path, "wb") as file:
218
+ if self.category in ['admin-settings']:
219
+ content = json.loads(res.content)
220
+ json.dump(content, file, indent=4)
221
+ else:
222
+ file.write(res.content)
223
+ success(f"{self.category} saved at {file_save_path}")
224
+ return True
225
+ error(f'Unable to download {self.category}')
226
+ except RequestException as e:
227
+ error(
228
+ f"Exception: Unable to download {self.category}. Reason: {e}"
229
+ )
230
+ return False
231
+
125
232
  def validate_input(self) -> ResponseType:
126
233
  """
127
234
  Validator
@@ -140,8 +247,34 @@ class Snapend:
140
247
  response['msg'] = \
141
248
  f"Invalid command. Valid commands are {', '.join(Snapend.SUBCOMMANDS)}."
142
249
  return response
143
- # Check sdk-download commands
144
- if self.subcommand == 'download':
250
+ if self.subcommand == 'enumerate':
251
+ if not self.game_id:
252
+ response['msg'] = "Missing required parameter: game_id"
253
+ return response
254
+ elif self.subcommand == 'clone':
255
+ if not self.game_id:
256
+ response['msg'] = "Missing required parameter: game_id"
257
+ return response
258
+ if not self.name:
259
+ response['msg'] = "Missing required parameter: name"
260
+ return response
261
+ if self.env.upper() not in Snapend.ENV_TYPES:
262
+ response['msg'] = (
263
+ "Invalid environment. Valid environments are "
264
+ f"{', '.join(Snapend.ENV_TYPES)}."
265
+ )
266
+ return response
267
+ if not self.manifest_path:
268
+ response['msg'] = "Missing required parameter: manifest_path"
269
+ return response
270
+ elif self.subcommand == 'apply':
271
+ if not self.manifest_path:
272
+ response['msg'] = "Missing required parameter: manifest_path"
273
+ return response
274
+ if not self.manifest_file_name:
275
+ response['msg'] = "Invalid manifest file. Supported formats are .json, .yml, .yaml"
276
+ return response
277
+ elif self.subcommand == 'download':
145
278
  if self.category not in Snapend.DOWNLOAD_CATEGORY:
146
279
  response['msg'] = (
147
280
  "Invalid SDK category. Valid categories are "
@@ -153,10 +286,10 @@ class Snapend:
153
286
  response['msg'] = "Invalid Download type."
154
287
  return response
155
288
  # Check file path
156
- if self.path and not os.path.isdir(f"{self.path}"):
289
+ if self.out_path and not os.path.isdir(f"{self.out_path}"):
157
290
  response['msg'] = (
158
- f"Invalid path {self.path}. "
159
- "Please enter a valid path to save your SDK"
291
+ f"Invalid path {self.out_path}. "
292
+ "Please enter a valid path to save your output file"
160
293
  )
161
294
  return response
162
295
  # Check the auth type
@@ -166,6 +299,10 @@ class Snapend:
166
299
  f"{', '.join(Snapend.AUTH_TYPES)}."
167
300
  )
168
301
  return response
302
+ elif self.subcommand == 'promote':
303
+ if not self.snapend_id:
304
+ response['msg'] = "Missing required parameter: snapend_id"
305
+ return response
169
306
  # Check update commands
170
307
  elif self.subcommand == 'update':
171
308
  byosnap_present = True
@@ -181,9 +318,10 @@ class Snapend:
181
318
  response['error'] = False
182
319
  return response
183
320
 
184
- def download(self) -> bool:
321
+ ## Subcommands ##
322
+ def enumerate(self) -> bool:
185
323
  """
186
- Download SDKs, Protos and Admin Settings
324
+ List Snapends
187
325
  """
188
326
  with Progress(
189
327
  SpinnerColumn(),
@@ -191,46 +329,217 @@ class Snapend:
191
329
  transient=True,
192
330
  ) as progress:
193
331
  progress.add_task(
194
- description=f'Downloading your Custom {self.category}...', total=None)
332
+ description='Enumerating all your game snapends...', total=None)
195
333
  try:
196
- url = (
197
- f"{self.base_url}/v1/snapser-api/snapends/{self.snapend_id}/"
198
- f"download?category={self.category}"
199
- )
200
- if self.category not in Snapend.DOWNLOAD_TYPE_NOT_REQUIRED:
201
- url += (
202
- f"&type={self.download_types[self.platform_type]['type']}"
203
- f"&subtype={self.download_types[self.platform_type]['subtype']}"
204
- )
205
- url_auth_type: str = 'user'
206
- if self.category == 'server-sdk' and self.auth_type == 'app':
207
- url_auth_type = 'app'
208
- url += f"&auth_type={url_auth_type}"
209
- if self.snaps:
210
- url += f"&snaps={self.snaps}"
334
+ url = f"{self.base_url}/v1/snapser-api/snapends?game_id={self.game_id}"
211
335
  res = requests.get(
212
- url, headers={'api-key': self.api_key}, timeout=SERVER_CALL_TIMEOUT
336
+ url, headers={'api-key': self.api_key},
337
+ timeout=SERVER_CALL_TIMEOUT
213
338
  )
214
- fn: str = f"snapser-{self.snapend_id}-admin-settings.json"
215
- if self.category != 'admin-settings':
216
- fn = f"snapser-{self.snapend_id}-{self.category}-{self.platform_type}-{self.auth_type}.zip"
217
- file_path_symbol = '/'
218
- if platform == 'win32':
219
- file_path_symbol = '\\'
220
- if self.path is not None:
221
- sdk_save_path = f"{self.path}{file_path_symbol}{fn}"
222
- else:
223
- sdk_save_path = f"{os.getcwd()}{file_path_symbol}{fn}"
339
+ response_json = res.json()
224
340
  if res.ok:
225
- with open(sdk_save_path, "wb") as file:
226
- file.write(res.content)
227
- success(f"SDK saved at {sdk_save_path}")
228
- return True
229
- error('Unable to download your custom SDK')
341
+ if 'clusters' in response_json:
342
+ success(response_json['clusters'])
343
+ return True
344
+ error(response_json)
345
+ except RequestException as e:
346
+ error(f"Exception: Unable to update your snapend {e}")
347
+ return False
348
+
349
+ def clone(self) -> bool:
350
+ """
351
+ Create a Snapend from a manifest
352
+ """
353
+ with Progress(
354
+ SpinnerColumn(),
355
+ TextColumn("[progress.description]{task.description}"),
356
+ transient=True,
357
+ ) as progress:
358
+ progress.add_task(
359
+ description='Applying your manifest...', total=None)
360
+ try:
361
+ with open(self.manifest_path, 'rb') as file:
362
+ files = {'snapend-manifest': file}
363
+ payload = {
364
+ 'game_id': self.game_id,
365
+ 'name': self.name,
366
+ 'env': self.env.upper(),
367
+ 'ext': self.manifest_file_name.split('.')[-1]
368
+ }
369
+ url = f"{self.base_url}/v1/snapser-api/snapends/snapend-manifest"
370
+ res = requests.post(
371
+ url, headers={'api-key': self.api_key},
372
+ files=files, data=payload, timeout=SERVER_CALL_TIMEOUT
373
+ )
374
+ if res.ok:
375
+ # extract the cluster ID
376
+ response = res.json()
377
+ if 'cluster' not in response or 'id' not in response['cluster']:
378
+ error(
379
+ 'Something went wrong. Please try again in sometime.'
380
+ )
381
+ return False
382
+ self._assign_snapend_id(response['cluster']['id'])
383
+ info(
384
+ f"Cluster ID assigned: {response['cluster']['id']}")
385
+ if self.blocking:
386
+ status = self._blocking_get_status()
387
+ # Fetch the new manifest
388
+ if status is True:
389
+ # TODO: Uncomment this if we want to do an auto download
390
+ # self._setup_for_download(
391
+ # self.manifest_file_name.split('.')[-1])
392
+ # self._execute_download()
393
+ info(
394
+ 'Do not forget to download the latest manifest.'
395
+ )
396
+ return True
397
+ info(
398
+ 'Snapend clone has been initiated but the Snapend is not up yet.'
399
+ 'Please try checking the status of the Snapend in some time'
400
+ )
401
+ return False
402
+ info(
403
+ "Snapend clone has been initiated. "
404
+ "You can check the status using "
405
+ f"`snapctl snapend state --snapend-id {response['cluster']['id']}`"
406
+ )
407
+ return True
408
+ error('Unable to apply the manifest. Reason: ' + res.text)
409
+ except RequestException as e:
410
+ error(f"Exception: Unable to apply the manifest snapend {e}")
411
+ return False
412
+
413
+ def apply(self) -> bool:
414
+ """
415
+ Apply a manifest
416
+ """
417
+ with Progress(
418
+ SpinnerColumn(),
419
+ TextColumn("[progress.description]{task.description}"),
420
+ transient=True,
421
+ ) as progress:
422
+ progress.add_task(
423
+ description='Applying your manifest...', total=None)
424
+ try:
425
+ with open(self.manifest_path, 'rb') as file:
426
+ files = {'snapend-manifest': file}
427
+ payload = {
428
+ 'ext': self.manifest_file_name.split('.')[-1]
429
+ }
430
+ url = f"{self.base_url}/v1/snapser-api/snapends/snapend-manifest"
431
+ res = requests.put(
432
+ url, headers={'api-key': self.api_key},
433
+ files=files, data=payload, timeout=SERVER_CALL_TIMEOUT
434
+ )
435
+ if res.ok:
436
+ # extract the cluster ID
437
+ response = res.json()
438
+ if 'cluster' not in response or 'id' not in response['cluster']:
439
+ error(
440
+ 'Something went wrong. Please try again in sometime.'
441
+ )
442
+ return False
443
+ self._assign_snapend_id(response['cluster']['id'])
444
+ if self.blocking:
445
+ status = self._blocking_get_status()
446
+ # Fetch the new manifest
447
+ if status is True:
448
+ # TODO: Uncomment this if we want to do an auto download
449
+ # self._setup_for_download(
450
+ # self.manifest_file_name.split('.')[-1])
451
+ # self._execute_download()
452
+ info(
453
+ 'Do not forget to download the latest manifest.'
454
+ )
455
+ return True
456
+ info(
457
+ 'Snapend apply has been initiated but the Snapend is not up yet.'
458
+ 'Please try checking the status of the Snapend in some time'
459
+ )
460
+ return False
461
+ info(
462
+ "Snapend apply has been initiated. "
463
+ "You can check the status using "
464
+ f"`snapctl snapend state --snapend-id {response['cluster']['id']}`"
465
+ )
466
+ return True
467
+ error('Unable to apply the manifest. Reason: ' + res.text)
468
+ except RequestException as e:
469
+ error(f"Exception: Unable to apply the manifest snapend {e}")
470
+ return False
471
+
472
+ def promote(self) -> bool:
473
+ """
474
+ Promote a staging manifest to production
475
+ """
476
+ with Progress(
477
+ SpinnerColumn(),
478
+ TextColumn("[progress.description]{task.description}"),
479
+ transient=True,
480
+ ) as progress:
481
+ progress.add_task(
482
+ description='Promoting your staging snapend...', total=None)
483
+ try:
484
+ with open(self.manifest_path, 'rb') as file:
485
+ payload = {
486
+ 'snapend_id': self.snapend_id
487
+ }
488
+ url = f"{self.base_url}/v1/snapser-api/snapends/promote"
489
+ res = requests.put(
490
+ url, headers={'api-key': self.api_key},
491
+ json=payload, timeout=SERVER_CALL_TIMEOUT
492
+ )
493
+ if res.ok:
494
+ # extract the cluster ID
495
+ response = res.json()
496
+ if 'cluster' not in response or 'id' not in response['cluster']:
497
+ error(
498
+ 'Something went wrong. Please try again in sometime.'
499
+ )
500
+ return False
501
+ self._assign_snapend_id(response['cluster']['id'])
502
+ if self.blocking:
503
+ status = self._blocking_get_status()
504
+ if status is True:
505
+ # TODO: Uncomment this if we want to do an auto download
506
+ # self._setup_for_download(
507
+ # self.manifest_file_name.split('.')[-1])
508
+ # self._execute_download()
509
+ # Fetch the new manifest
510
+ info(
511
+ 'Do not forget to download the latest manifest.'
512
+ )
513
+ return True
514
+ info(
515
+ 'Snapend apply has been initiated but the Snapend is not up yet.'
516
+ 'Please try checking the status of the Snapend in some time'
517
+ )
518
+ return False
519
+ info(
520
+ "Snapend apply has been initiated. "
521
+ "You can check the status using "
522
+ f"`snapctl snapend state --snapend-id {response['cluster']['id']}`"
523
+ )
524
+ return True
525
+ error('Unable to promote the manifest. Reason: ' + res.text)
230
526
  except RequestException as e:
231
- error(f"Exception: Unable to download the SDK {e}")
527
+ error(f"Exception: Unable to apply the manifest snapend {e}")
232
528
  return False
233
529
 
530
+ def download(self) -> bool:
531
+ """
532
+ Download SDKs, Protos, Admin Settings and Configuration
533
+ """
534
+ with Progress(
535
+ SpinnerColumn(),
536
+ TextColumn("[progress.description]{task.description}"),
537
+ transient=True,
538
+ ) as progress:
539
+ progress.add_task(
540
+ description=f'Downloading your Custom {self.category}...', total=None)
541
+ return self._execute_download()
542
+
234
543
  def update(self) -> bool:
235
544
  """
236
545
  Update a Snapend
@@ -1,11 +1,14 @@
1
1
  """
2
2
  Constants used by snapctl
3
3
  """
4
- VERSION = '0.22.3'
4
+ COMPANY_NAME = 'Snapser'
5
+ VERSION = '0.26.1'
5
6
  CONFIG_FILE_MAC = '~/.snapser/config'
6
- CONFIG_FILE_WIN = '%USERPROFILE%\.snapser\config'
7
+ CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
8
+
7
9
  DEFAULT_PROFILE = 'default'
8
10
  API_KEY = 'SNAPSER_API_KEY'
11
+ URL_KEY = 'SNAPSER_URL_KEY'
9
12
  SERVER_CALL_TIMEOUT = 30
10
13
 
11
14
  SNAPCTL_SUCCESS = 0
@@ -3,6 +3,7 @@ This file contains the endpoints for the Snapser API.
3
3
  """
4
4
  END_POINTS = {
5
5
  'DEV': 'https://gateway.dev.snapser.io/snapser',
6
+ 'DEV_TWO': 'https://gateway.dev.snapser.io/devtwo',
6
7
  'PLAYTEST': 'https://gateway.dev.snapser.io/playtest',
7
8
  'PROD': 'https://gateway.snapser.com/snapser'
8
9
  }
snapctl/config/hashes.py CHANGED
@@ -126,6 +126,17 @@ PROTOS_TYPES: dict[str, dict[str, str]] = {
126
126
  },
127
127
  }
128
128
 
129
+ SNAPEND_MANIFEST_TYPES: dict[str, dict[str, str]] = {
130
+ 'json': {
131
+ 'type': 'json',
132
+ 'subtype': '',
133
+ },
134
+ 'yaml': {
135
+ 'type': 'yaml',
136
+ 'subtype': '',
137
+ },
138
+ }
139
+
129
140
  SERVICE_IDS = [
130
141
  'analytics', 'auth', 'client-logs', 'events', 'experiments', 'gdpr', 'guilds', 'hades', 'iap',
131
142
  'inventory', 'leaderboards', 'matchmaking', 'notifications', 'parties', 'profiles', 'quests',
snapctl/main.py CHANGED
@@ -6,23 +6,43 @@ import os
6
6
  from sys import platform
7
7
  from typing import Union, Callable
8
8
  import typer
9
+ import pyfiglet
9
10
 
10
11
  from snapctl.commands.byosnap import ByoSnap
11
12
  from snapctl.commands.byogs import ByoGs
13
+ from snapctl.commands.game import Game
12
14
  from snapctl.commands.snapend import Snapend
13
- from snapctl.config.constants import API_KEY, CONFIG_FILE_MAC, CONFIG_FILE_WIN, DEFAULT_PROFILE, \
14
- VERSION, SNAPCTL_SUCCESS, SNAPCTL_ERROR
15
+ from snapctl.config.constants import COMPANY_NAME, API_KEY, URL_KEY, CONFIG_FILE_MAC, \
16
+ CONFIG_FILE_WIN, DEFAULT_PROFILE, VERSION, SNAPCTL_SUCCESS, SNAPCTL_ERROR
15
17
  from snapctl.config.endpoints import END_POINTS
16
- from snapctl.config.hashes import CLIENT_SDK_TYPES, SERVER_SDK_TYPES, PROTOS_TYPES, SERVICE_IDS
18
+ from snapctl.config.hashes import CLIENT_SDK_TYPES, SERVER_SDK_TYPES, PROTOS_TYPES, SERVICE_IDS, \
19
+ SNAPEND_MANIFEST_TYPES
17
20
  from snapctl.types.definitions import ResponseType
18
21
  from snapctl.utils.echo import error, success, info
19
22
 
20
- app = typer.Typer()
23
+ ######### Globals #########
24
+
25
+
26
+ def draw_ascii_text():
27
+ """
28
+ Draws the ascii text for Snapser
29
+ """
30
+ ascii_text = pyfiglet.figlet_format(COMPANY_NAME)
31
+ typer.echo(ascii_text)
32
+
33
+
34
+ app = typer.Typer(
35
+ help=draw_ascii_text(),
36
+ context_settings={
37
+ "help_option_names": ["-h", "--help"]
38
+ }
39
+ )
40
+
21
41
 
22
42
  ######### HELPER METHODS #########
23
43
 
24
44
 
25
- def extract_api_key(profile: str | None = None) -> object:
45
+ def extract_config(extract_key: str, profile: str | None = None) -> object:
26
46
  """
27
47
  Extracts the API Key from the environment variable and if not present from the config file
28
48
  """
@@ -31,7 +51,7 @@ def extract_api_key(profile: str | None = None) -> object:
31
51
  'value': None
32
52
  }
33
53
  # Option 1
34
- env_api_key = os.getenv(API_KEY)
54
+ env_api_key = os.getenv(extract_key)
35
55
  if env_api_key is not None:
36
56
  result['location'] = 'environment-variable'
37
57
  result['value'] = env_api_key
@@ -50,11 +70,11 @@ def extract_api_key(profile: str | None = None) -> object:
50
70
  config.read(config_file_path, encoding=encoding)
51
71
  config_profile: str = DEFAULT_PROFILE
52
72
  if profile is not None and profile != '' and profile != DEFAULT_PROFILE:
53
- result['location'] = f'config-file:profile:{profile}'
73
+ result['location'] = f'{config_file_path}:profile {profile}'
54
74
  config_profile = f'profile {profile}'
55
- info(f"Trying to extract API KEY from from profile {profile}")
75
+ info(f"Trying to extract API KEY from profile {profile}")
56
76
  result['value'] = config.get(
57
- config_profile, API_KEY, fallback=None, raw=True
77
+ config_profile, extract_key, fallback=None, raw=True
58
78
  )
59
79
  else:
60
80
  error(
@@ -66,10 +86,17 @@ def get_base_url(api_key: str | None) -> str:
66
86
  """
67
87
  Returns the base url based on the api_key
68
88
  """
89
+ # Check if the user has a URL override
90
+ url_key_obj = extract_config(URL_KEY, None)
91
+ if url_key_obj['value'] is not None:
92
+ return url_key_obj['value']
93
+ # If there was no override then we use the default
69
94
  if api_key is None:
70
95
  return ''
71
96
  if api_key.startswith('dev_'):
72
97
  return END_POINTS['DEV']
98
+ if api_key.startswith('devtwo_'):
99
+ return END_POINTS['DEV_TWO']
73
100
  if api_key.startswith('playtest_'):
74
101
  return END_POINTS['PLAYTEST']
75
102
  return END_POINTS['PROD']
@@ -103,7 +130,7 @@ def default_context_callback(ctx: typer.Context):
103
130
  # Ensure ctx object is instantiated
104
131
  ctx.ensure_object(dict)
105
132
  # Extract the api_key
106
- api_key_obj = extract_api_key()
133
+ api_key_obj = extract_config(API_KEY, None)
107
134
  ctx.obj['version'] = VERSION
108
135
  ctx.obj['api_key'] = api_key_obj['value']
109
136
  ctx.obj['api_key_location'] = api_key_obj['location']
@@ -144,7 +171,7 @@ def profile_context_callback(
144
171
  # info("In Profile Callback")
145
172
  # Ensure ctx object is instantiated
146
173
  ctx.ensure_object(dict)
147
- api_key_obj = extract_api_key(profile)
174
+ api_key_obj = extract_config(API_KEY, profile)
148
175
  if api_key_obj['value'] is None and profile is not None and profile != '':
149
176
  conf_file = ''
150
177
  if platform == 'win32':
@@ -233,7 +260,9 @@ def byosnap(
233
260
  ),
234
261
  # publish-image and publish-version
235
262
  tag: str = typer.Option(
236
- None, "--tag", help="(req: build, push publish-image and publish-version) Tag for your snap"
263
+ None, "--tag", help=(
264
+ "(req: build, push publish-image and publish-version) Tag for your snap"
265
+ )
237
266
  ),
238
267
  # publish-image
239
268
  path: Union[str, None] = typer.Option(
@@ -360,6 +389,44 @@ def byogs(
360
389
  success(f"BYOGs {subcommand} complete")
361
390
 
362
391
 
392
+ @app.command()
393
+ def game(
394
+ ctx: typer.Context,
395
+ # Required fields
396
+ subcommand: str = typer.Argument(
397
+ ..., help="Game Subcommands: " + ", ".join(Game.SUBCOMMANDS) + "."
398
+ ),
399
+ # name
400
+ name: str = typer.Option(
401
+ None, "--name",
402
+ help=("(req: create) Name of your game: ")
403
+ ),
404
+ # overrides
405
+ api_key: Union[str, None] = typer.Option(
406
+ None, "--api-key", help="API Key override.", callback=api_key_context_callback
407
+ ),
408
+ profile: Union[str, None] = typer.Option(
409
+ None, "--profile", help="Profile to use.", callback=profile_context_callback
410
+ ),
411
+ ) -> None:
412
+ """
413
+ Game commands
414
+ """
415
+ validate_command_context(ctx)
416
+ game_obj: Game = Game(
417
+ subcommand, ctx.obj['base_url'], ctx.obj['api_key'], name)
418
+ validate_input_response: ResponseType = game_obj.validate_input()
419
+ if validate_input_response['error']:
420
+ error(validate_input_response['msg'])
421
+ raise typer.Exit(SNAPCTL_ERROR)
422
+ command_method = subcommand.replace('-', '_')
423
+ method: Callable[..., bool] = getattr(game_obj, command_method)
424
+ if not method():
425
+ error(f"Game {subcommand} failed")
426
+ raise typer.Exit(SNAPCTL_ERROR)
427
+ success(f"Game {subcommand} complete")
428
+
429
+
363
430
  @app.command()
364
431
  def snapend(
365
432
  ctx: typer.Context,
@@ -367,7 +434,21 @@ def snapend(
367
434
  subcommand: str = typer.Argument(
368
435
  ..., help="Snapend Subcommands: " + ", ".join(Snapend.SUBCOMMANDS) + "."
369
436
  ),
370
- snapend_id: str = typer.Argument(..., help="Snapend Id"),
437
+ # snapend_id: str = typer.Argument(..., help="Snapend Id"),
438
+ snapend_id: str = typer.Option(
439
+ None, "--snapend-id",
440
+ help=("(req: update, download) Snapend Id")
441
+ ),
442
+ # enumerate
443
+ game_id: str = typer.Option(
444
+ None, "--game-id",
445
+ help="(req: enumerate, clone) Game Id"
446
+ ),
447
+ # apply, clone
448
+ manifest_path: str = typer.Option(
449
+ None, "--manifest-path",
450
+ help="(req: apply|clone) Path to the manifest file"
451
+ ),
371
452
  # download
372
453
  category: str = typer.Option(
373
454
  None, "--category",
@@ -376,22 +457,22 @@ def snapend(
376
457
  ", ".join(Snapend.DOWNLOAD_CATEGORY) + "."
377
458
  )
378
459
  ),
379
- path: Union[str, None] = typer.Option(
380
- None, "--path", help="(req: download) Path to save the SDK"),
381
460
  platform_type: str = typer.Option(
382
461
  None, "--type",
383
462
  help=(
384
463
  "(req: --category client-sdk|server-sdk|protos --type ) "
385
464
  "SDK Types: client-sdk(" + ", ".join(CLIENT_SDK_TYPES.keys()) +
386
465
  ") server-sdk(" + ", ".join(SERVER_SDK_TYPES.keys()) +
387
- ") protos(" + ", ".join(PROTOS_TYPES.keys()) + ")"
466
+ ") protos(" + ", ".join(PROTOS_TYPES.keys()) + ")" +
467
+ ") snapend-manifest(" + \
468
+ ", ".join(SNAPEND_MANIFEST_TYPES.keys()) + ")"
388
469
  )
389
470
  ),
390
471
  auth_type: str = typer.Option(
391
472
  'user', "--auth-type",
392
473
  help=(
393
474
  "(optional: download) Only applicable for --category server-sdk --auth-type"
394
- "Auth-Types: ()" + ", ".join(Snapend.AUTH_TYPES) + ")"
475
+ "Auth-Types: (" + ", ".join(Snapend.AUTH_TYPES) + ")"
395
476
  )
396
477
  ),
397
478
  snaps: Union[str, None] = typer.Option(
@@ -402,6 +483,17 @@ def snapend(
402
483
  "snaps(" + ", ".join(SERVICE_IDS)
403
484
  )
404
485
  ),
486
+ # Clone
487
+ name: Union[str, None] = typer.Option(
488
+ None, "--name", help="(req: clone) Snapend name"),
489
+ env: Union[str, None] = typer.Option(
490
+ None, "--env", help=(
491
+ "(req: clone) Snapend environment"
492
+ "Environments: (" + ", ".join(Snapend.ENV_TYPES) + ")"
493
+ )),
494
+ # Download, Apply, Clone
495
+ out_path: Union[str, None] = typer.Option(
496
+ None, "--out-path", help="(optional: download|apply|clone) Path to save the output file"),
405
497
  # update
406
498
  byosnaps: str = typer.Option(
407
499
  None, "--byosnaps",
@@ -417,6 +509,7 @@ def snapend(
417
509
  "Eg: fleet-1:service-1:v1.0.0,fleet-2:service-2:v1.0.0"
418
510
  )
419
511
  ),
512
+ # create, update, promote, apply, clone
420
513
  blocking: bool = typer.Option(
421
514
  False, "--blocking",
422
515
  help=(
@@ -437,9 +530,19 @@ def snapend(
437
530
  """
438
531
  validate_command_context(ctx)
439
532
  snapend_obj: Snapend = Snapend(
440
- subcommand, ctx.obj['base_url'], ctx.obj['api_key'],
441
- snapend_id, category, platform_type, auth_type,
442
- path, snaps, byosnaps, byogs, blocking
533
+ subcommand, ctx.obj['base_url'], ctx.obj['api_key'], snapend_id,
534
+ # Enumerate, Clone
535
+ game_id,
536
+ # Clone
537
+ name, env,
538
+ # Apply, Clone
539
+ manifest_path,
540
+ # Download
541
+ category, platform_type, auth_type, snaps,
542
+ # Download, Apply and Clone
543
+ out_path,
544
+ # Update
545
+ byosnaps, byogs, blocking
443
546
  )
444
547
  validate_input_response: ResponseType = snapend_obj.validate_input()
445
548
  if validate_input_response['error']:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: snapctl
3
- Version: 0.22.3
3
+ Version: 0.26.1
4
4
  Summary: Snapser CLI Tool
5
5
  Author: Ajinkya Apte
6
6
  Author-email: aj@snapser.com
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Dist: configparser (>=6.0.0,<7.0.0)
13
+ Requires-Dist: pyfiglet (>=1.0.2,<2.0.0)
13
14
  Requires-Dist: requests (>=2.28.2,<3.0.0)
14
15
  Requires-Dist: typer[all] (>=0.7.0,<0.8.0)
15
16
  Description-Content-Type: text/markdown
@@ -392,7 +393,7 @@ snapctl byogs publish-version --help
392
393
  snapctl byogs publish-version $byogs_sid --tag $image_tag --prefix $prefix --version $version --http-port $ingress_port
393
394
  ```
394
395
 
395
- ### Snapend
396
+ ### Game
396
397
 
397
398
  #### 1. snapend help
398
399
 
@@ -403,27 +404,89 @@ See all the supported commands
403
404
  snapctl snapend --help
404
405
  ```
405
406
 
407
+ #### 2. Create a game
408
+ Create a game
409
+ ```
410
+ snapctl game create --name $gameName
411
+ ```
412
+
413
+ #### 3. Enumerate games
414
+ List all the games
415
+ ```
416
+ snapctl game enumerate
417
+ ```
418
+
419
+
420
+ ### Snapend
421
+
422
+ #### 1. snapend help
423
+
424
+ See all the supported commands
425
+
426
+ ```
427
+ # Help for the snapend command
428
+ snapctl snapend --help
429
+ ```
430
+
406
431
  #### 2. Snapend Downloads
407
432
 
408
- Download SDKs and Protos for your Snapend
433
+ Download Manifest, SDKs and Protos for your Snapend
409
434
 
410
435
  ```
411
- # Help for the byogs command
436
+ # Help for the download command
412
437
  snapctl snapend download --help
413
438
 
414
439
  # Download your Snapend SDK and Protos
415
440
  # $snapend_id = Cluster Id
416
- # $category = client-sdk, server-sdk, protos, admin-settings
417
- # $sdk_type = One of the supported SDK names:
418
- # client-sdk(unity, unreal, roblox, godot, cocos, ios-objc, ios-swift, android-java, android-kotlin, web-ts, web-js),
419
- # server-sdk(csharp, cpp, lua, ts, go, python, kotlin, java, c, node, js, perl, php, closure, ruby, rust),
420
- # protos(go)
441
+ # $category = snapend-manifest, client-sdk, server-sdk, protos
442
+ # $type = One of the supported types:
443
+ # snapend-manifest(yaml, json)
444
+ # client-sdk(unity, unreal, roblox, godot, cocos, ios-objc, ios-swift, android-java, android-kotlin, web-ts, web-js),
445
+ # server-sdk(csharp, cpp, lua, ts, go, python, kotlin, java, c, node, js, perl, php, closure, ruby, rust),
446
+ # protos(go, csharp, cpp, python)
421
447
  # Example:
448
+ # snapctl snapend download gx5x6bc0 --category snapend-manifest --type yaml
422
449
  # snapctl snapend download gx5x6bc0 --category client-sdk --type unity
423
- snapctl snapend download $snapend_id --category $category --type $sdk_type
450
+ snapctl snapend download $snapend_id --category $category --type $type
451
+ ```
452
+
453
+ #### 3. Clone Snapend
454
+
455
+ Clone a Snapend from an existing manifest. Passing the blocking flag ensures your CLI command waits till the new Snapend is up.
456
+
457
+ ```
458
+ # Help for the download command
459
+ snapctl snapend clone --help
460
+
461
+ # Download your Snapend SDK and Protos
462
+ # $gameId = Game Id
463
+ # $snapendName = Name of your new Snapend
464
+ # $env = One of development, staging
465
+ # $pathToManifest = Path to the manifest file; should include the file name
466
+ # Example:
467
+ # snapctl snapend clone --game-id 2581d802-aca-496c-8a76-1953ad0db165 --name new-snapend --env development --manifest-path "C:\Users\name\Downloads\snapser-ox1bcyim-manifest.json" --blocking
468
+ snapctl snapend clone --game-id $gameId --name $snapendName --env $env --manifest-path "$pathToManifest"
469
+ snapctl snapend clone --game-id $gameId --name $snapendName --env $env --manifest-path "$pathToManifest" --blocking
470
+ ```
471
+
472
+ #### 4. Apply Snapend Changes
473
+
474
+ Apply changes to your Snapend from a manifest. You should have the latest manifest before applying changes. This is to prevent
475
+ a user stomping over someone elses changes. Passing the blocking flag ensures your CLI command waits till the update is complete.
476
+
477
+ ```
478
+ # Help for the download command
479
+ snapctl snapend apply --help
480
+
481
+ # Download your Snapend SDK and Protos
482
+ # $pathToManifest = Path to the manifest file; should include the file name
483
+ # Example:
484
+ # snapctl snapend apply --manifest-path "C:\Users\name\Downloads\snapser-ox1bcyim-manifest.json" --blocking
485
+ snapctl snapend apply --manifest-path "$pathToManifest"
486
+ snapctl snapend apply --manifest-path "$pathToManifest" --blocking
424
487
  ```
425
488
 
426
- #### 3. Update Snapend BYOSnap or BYOGs versions
489
+ #### 5. Update Snapend BYOSnap or BYOGs versions
427
490
 
428
491
  Update your BYOSnap or BYOGs versions for the Snapend
429
492
 
@@ -443,7 +506,7 @@ snapctl snapend update --help
443
506
  snapctl snapend update $snapend_id --byosnaps $byosnaps --byogs $byogs --blocking
444
507
  ```
445
508
 
446
- #### 4. Get the Snapend state
509
+ #### 6. Get the Snapend state
447
510
 
448
511
  Get the Snapend state
449
512
 
@@ -0,0 +1,21 @@
1
+ snapctl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ snapctl/__main__.py,sha256=43jKoTk8b85hk_MT6499N3ruHdEfM8WBImd_-3VzjI8,116
3
+ snapctl/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ snapctl/commands/byogs.py,sha256=S77gBVT7W-6Ph7D8YU_2YfFNFHhlnBYYg3hafuw_w5Q,19216
5
+ snapctl/commands/byosnap.py,sha256=RQv3r30dkz8n1BgEnE-tAnAzPLoz-CLEQoiy1WKZQeg,23172
6
+ snapctl/commands/game.py,sha256=rnsTqrXavE5D-KULXkDBQ-PBI8sSKK4it3S_YMJfiUY,3633
7
+ snapctl/commands/snapend.py,sha256=Wyw9XEYn5b8YdllJumhsaoXqNeEA6XNxGRxToqmgf5o,25263
8
+ snapctl/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ snapctl/config/constants.py,sha256=Lgp7ddPGBBtAaqdFPk7VRV0FIHmeSYWRvXUPVBz8m40,525
10
+ snapctl/config/endpoints.py,sha256=VAeOmx3k3ukB-9XuGI65KtCJwFK-KFgzor-UWE8JU0g,297
11
+ snapctl/config/hashes.py,sha256=R0t5xwY6DJpwiJZfo1SFh2qvADmDIrdPV7lD_IKbnJg,2889
12
+ snapctl/main.py,sha256=aSlAfOj3Lrb4gw5QHTWQ1gqCUHgtZ39GJ8SHxliTDik,18516
13
+ snapctl/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ snapctl/types/definitions.py,sha256=rkRyTBHzeQtCt_uYObgYvL5txnVq8r_n5g4IUAq2FWc,233
15
+ snapctl/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ snapctl/utils/echo.py,sha256=0nhWYDBQTfZGtl8EK21hJhj0now2rQmCT-yYMCKupy4,584
17
+ snapctl/utils/helper.py,sha256=xasSsg-0DgHT-TWejh2IXW-s9AYntO1D2O6Uy6BQLcE,1381
18
+ snapctl-0.26.1.dist-info/METADATA,sha256=7nVCIinJ40Wa14vFcSeWFVq2wdIax7l4eiUPBXrZZ6U,15246
19
+ snapctl-0.26.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
20
+ snapctl-0.26.1.dist-info/entry_points.txt,sha256=tkKW9MzmFdRs6Bgkv29G78i9WEBK4WIOWunPfe3t2Wg,44
21
+ snapctl-0.26.1.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- snapctl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- snapctl/__main__.py,sha256=43jKoTk8b85hk_MT6499N3ruHdEfM8WBImd_-3VzjI8,116
3
- snapctl/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- snapctl/commands/byogs.py,sha256=hXRyqEYb_k3gdqmbXn1TqHYa0dDp6yOGQIgvxY03OC8,19077
5
- snapctl/commands/byosnap.py,sha256=Yf9f5Os6vdfHBRVKx_E4IqAGctAtf32QobqUG0fBukw,23033
6
- snapctl/commands/snapend.py,sha256=fX35lRZwqs2VSyu8XZL4FGrWmWSN6uFIF7L4qraV5K4,11420
7
- snapctl/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- snapctl/config/constants.py,sha256=t7i5_OgJmURLXxfv_QKUbsj7mC9AoiOUMjhM5cGM0Ug,472
9
- snapctl/config/endpoints.py,sha256=7aSu-tWwF4NcSh1eEpNlAa-Cjg2mitBLudSf41Wb47M,241
10
- snapctl/config/hashes.py,sha256=yf2800SRpq1pr0BrEuj4CndoyZA1i2jrhg7dLRWNp7Y,2696
11
- snapctl/main.py,sha256=3k7aLK3kcL8AX0CLD9LD7e0-kX6mamruEIYn8Nbpi3o,15356
12
- snapctl/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- snapctl/types/definitions.py,sha256=rkRyTBHzeQtCt_uYObgYvL5txnVq8r_n5g4IUAq2FWc,233
14
- snapctl/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- snapctl/utils/echo.py,sha256=0nhWYDBQTfZGtl8EK21hJhj0now2rQmCT-yYMCKupy4,584
16
- snapctl/utils/helper.py,sha256=xasSsg-0DgHT-TWejh2IXW-s9AYntO1D2O6Uy6BQLcE,1381
17
- snapctl-0.22.3.dist-info/METADATA,sha256=PE7iPefecHhnAIA1bZKHqOWiotsDMeadf0gOKLIrB2g,13204
18
- snapctl-0.22.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
19
- snapctl-0.22.3.dist-info/entry_points.txt,sha256=tkKW9MzmFdRs6Bgkv29G78i9WEBK4WIOWunPfe3t2Wg,44
20
- snapctl-0.22.3.dist-info/RECORD,,