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

@@ -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)
@@ -3,7 +3,7 @@ Constants used by snapctl
3
3
  """
4
4
  COMPANY_NAME = 'Snapser'
5
5
  VERSION_PREFIX = ''
6
- VERSION = '1.0.4'
6
+ VERSION = '1.1.1'
7
7
  CONFIG_FILE_MAC = '~/.snapser/config'
8
8
  CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
9
9
 
@@ -25,7 +25,9 @@ HTTP_CONFLICT = 409
25
25
 
26
26
  # HTTP Error codes
27
27
  HTTP_ERROR_GAME_LIMIT_REACHED = 520
28
+ HTTP_ERROR_GAME_NOT_FOUND = 521
28
29
  HTTP_ERROR_DUPLICATE_GAME_NAME = 523
30
+ HTTP_ERROR_CLUSTER_UPDATE_IN_PROGRESS = 535
29
31
  HTTP_ERROR_RESOURCE_NOT_FOUND = 541
30
32
  HTTP_ERROR_SERVICE_VERSION_EXISTS = 542
31
33
  HTTP_ERROR_SERVICE_IN_USE = 543
@@ -38,12 +40,23 @@ SNAPCTL_SUCCESS = 0
38
40
  SNAPCTL_ERROR = 1
39
41
  SNAPCTL_INPUT_ERROR = 2
40
42
  SNAPCTL_RESOURCE_NOT_FOUND = 3
43
+ SNAPCTL_INTERNAL_SERVER_ERROR = 4
41
44
 
42
- # Configuration Errors - 10 - 19
45
+ # Configuration Errors - 10 - 12
43
46
  SNAPCTL_CONFIGURATION_INCORRECT = 10
44
47
  SNAPCTL_CONFIGURATION_ERROR = 11
45
48
  SNAPCTL_DEPENDENCY_MISSING = 12
46
49
 
50
+ # Snaps Errors - 13 - 15
51
+ SNAPCTL_SNAPS_GENERIC_ERROR = 13
52
+ SNAPCTL_SNAPS_ENUMERATE_ERROR = 14
53
+
54
+ # Snapend Manifest Errors - 16 - 19
55
+ SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR = 16
56
+ SNAPCTL_SNAPEND_MANIFEST_SYNC_ERROR = 17
57
+ SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR = 18
58
+ SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR = 19
59
+
47
60
  # BYOGS Errors - 20 - 29
48
61
  SNAPCTL_BYOGS_GENERIC_ERROR = 20
49
62
  SNAPCTL_BYOGS_DEPENDENCY_MISSING = 21
@@ -108,6 +121,10 @@ SNAPCTL_SNAPEND_UPDATE_SERVER_ERROR = 73
108
121
  SNAPCTL_SNAPEND_UPDATE_TIMEOUT_ERROR = 74
109
122
  SNAPCTL_SNAPEND_STATE_ERROR = 75
110
123
  SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR = 76
124
+ SNAPCTL_SNAPEND_CREATE_ERROR = 77
125
+ SNAPCTL_SNAPEND_CREATE_SERVER_ERROR = 78
126
+ SNAPCTL_SNAPEND_CREATE_TIMEOUT_ERROR = 79
127
+
111
128
 
112
129
  # Generate Errors - 80 - 85
113
130
  SNAPCTL_GENERATE_GENERIC_ERROR = 80
@@ -0,0 +1,20 @@
1
+ ## Release 1.1.0
2
+ ##### Oct 13, 2025
3
+
4
+ ### Deprecation Notice
5
+ 1. The `Snapend` command now accepts `--application-id` for consistency with the web app. The `--game-id` parameter is still supported but will be removed in future releases.
6
+ 2. The `Game` command has been renamed to `Application`. The old command is still supported but will be removed in future releases. Please use `snapctl application` instead of `snapctl game`.
7
+
8
+ ### New Features
9
+ #### Snapend command
10
+ - Added a new `snapend create` command to create a new Snapend instance using a manifest file.
11
+
12
+ #### Snaps command
13
+ - Added a new `snaps enumerate` command to get a list of all available snaps for your organization, including all the metadata and dependencies.
14
+
15
+ #### Snapend Manifest commands
16
+ - We have added a new command `snapend-manifest` that enables you to create CI/CD pipelines for your Snapend manifests. The command supports the following subcommands:
17
+ - `create`: Allows you to create a brand new manifest file by telling the command which snaps you want to include.
18
+ - `update`: Tells the snapctl to update your manifest file with a set of snaps and features. These are synced with the manifest file you pass.
19
+ - `upgrade`: Tells snapctl to upgrade all the snaps in the manifest or selected snaps to their latest versions.
20
+
@@ -0,0 +1,8 @@
1
+ ## Release 1.1.1
2
+ ##### Oct 15, 2025
3
+
4
+ ### Features
5
+ 1. Snapend manifest command now supports `sync` and `update` subcommands.
6
+
7
+ ### Bug Fixes
8
+ 1. The update command was always upgrading the auth snap to the latest version. This has now been fixed.
snapctl/main.py CHANGED
@@ -8,12 +8,15 @@ from typing import Union
8
8
  import typer
9
9
  import pyfiglet
10
10
 
11
+ from snapctl.commands.application import Application
11
12
  from snapctl.commands.byosnap import ByoSnap
12
13
  from snapctl.commands.byogs import ByoGs
13
14
  from snapctl.commands.game import Game
14
15
  from snapctl.commands.generate import Generate
15
16
  from snapctl.commands.snapend import Snapend
16
17
  from snapctl.commands.byows import Byows
18
+ from snapctl.commands.snaps import Snaps
19
+ from snapctl.commands.snapend_manifest import SnapendManifest
17
20
  from snapctl.commands.release_notes import ReleaseNotes
18
21
  from snapctl.config.constants import COMPANY_NAME, API_KEY, URL_KEY, CONFIG_FILE_MAC, \
19
22
  CONFIG_FILE_WIN, DEFAULT_PROFILE, VERSION, SNAPCTL_SUCCESS, CONFIG_PATH_KEY, \
@@ -21,7 +24,7 @@ from snapctl.config.constants import COMPANY_NAME, API_KEY, URL_KEY, CONFIG_FILE
21
24
  from snapctl.config.endpoints import END_POINTS, GATEWAY_END_POINTS
22
25
  from snapctl.config.hashes import PROTOS_TYPES, SERVICE_IDS, \
23
26
  SNAPEND_MANIFEST_TYPES, SDK_TYPES
24
- from snapctl.utils.echo import error, success, info
27
+ from snapctl.utils.echo import error, success, info, warning
25
28
  from snapctl.utils.helper import validate_api_key
26
29
  from snapctl.utils.telemetry import telemetry
27
30
 
@@ -504,8 +507,10 @@ def game(
504
507
  ),
505
508
  ) -> None:
506
509
  """
507
- Game commands
510
+ Game commands - DEPRECATED: Use Application commands instead
508
511
  """
512
+ warning(
513
+ "Game commands have been deprecated. Please use Application commands instead.")
509
514
  validate_command_context(ctx)
510
515
  game_obj: Game = Game(
511
516
  subcommand=subcommand,
@@ -518,6 +523,42 @@ def game(
518
523
  raise typer.Exit(code=SNAPCTL_SUCCESS)
519
524
 
520
525
 
526
+ @app.command()
527
+ @telemetry("application", subcommand_arg="subcommand")
528
+ def application(
529
+ ctx: typer.Context,
530
+ # Required fields
531
+ subcommand: str = typer.Argument(
532
+ ..., help="Application Subcommands: " + ", ".join(Application.SUBCOMMANDS) + "."
533
+ ),
534
+ # name
535
+ name: str = typer.Option(
536
+ None, "--name",
537
+ help=("(req: create) Name of your application: ")
538
+ ),
539
+ # overrides
540
+ api_key: Union[str, None] = typer.Option(
541
+ None, "--api-key", help="API Key override.", callback=api_key_context_callback
542
+ ),
543
+ profile: Union[str, None] = typer.Option(
544
+ None, "--profile", help="Profile from the Snapser config to use.", callback=profile_context_callback
545
+ ),
546
+ ) -> None:
547
+ """
548
+ Application commands
549
+ """
550
+ validate_command_context(ctx)
551
+ application_obj: Application = Application(
552
+ subcommand=subcommand,
553
+ base_url=ctx.obj['base_url'],
554
+ api_key=ctx.obj['api_key'],
555
+ name=name
556
+ )
557
+ getattr(application_obj, subcommand.replace('-', '_'))()
558
+ success(f"Application {subcommand} complete")
559
+ raise typer.Exit(code=SNAPCTL_SUCCESS)
560
+
561
+
521
562
  @app.command()
522
563
  @telemetry("generate", subcommand_arg="subcommand")
523
564
  def generate(
@@ -581,12 +622,16 @@ def snapend(
581
622
  # enumerate
582
623
  game_id: str = typer.Option(
583
624
  None, "--game-id",
584
- help="(req: enumerate, clone) Game Id"
625
+ help="(DEPRECATED: Use --application-id instead) Game Id"
626
+ ),
627
+ application_id: str = typer.Option(
628
+ None, "--application-id",
629
+ help="(req: enumerate, create, clone) Application Id"
585
630
  ),
586
631
  # apply, clone
587
632
  manifest_path_filename: str = typer.Option(
588
633
  None, "--manifest-path-filename",
589
- help="(req: apply|clone) Full Path to the manifest file including the filename."
634
+ help="(req: create|apply|clone) Full Path to the manifest file including the filename."
590
635
  ),
591
636
  force: bool = typer.Option(
592
637
  False, "--force",
@@ -627,7 +672,7 @@ def snapend(
627
672
  "--http-lib " + Snapend.get_http_formats_str()
628
673
  )
629
674
  ),
630
- snaps: Union[str, None] = typer.Option(
675
+ snaps_list_str: Union[str, None] = typer.Option(
631
676
  None, "--snaps",
632
677
  help=(
633
678
  "(optional: download) Comma separated list of snap ids to customize the "
@@ -637,15 +682,15 @@ def snapend(
637
682
  ),
638
683
  # Clone
639
684
  name: Union[str, None] = typer.Option(
640
- None, "--name", help="(req: clone) Snapend name"),
685
+ None, "--name", help="(req: clone, optional: create) Snapend name"),
641
686
  env: Union[str, None] = typer.Option(
642
687
  None, "--env", help=(
643
- "(req: clone) Snapend environment"
688
+ "(req: clone, optional: create) Snapend environment"
644
689
  "Environments: (" + ", ".join(Snapend.ENV_TYPES) + ")"
645
690
  )),
646
691
  # Download, Apply, Clone
647
692
  out_path: Union[str, None] = typer.Option(
648
- None, "--out-path", help="(optional: download|apply|clone) Path to save the output file"),
693
+ None, "--out-path", help="(optional: create|download|apply|clone) Path to save the output file"),
649
694
  # update
650
695
  byosnaps_list: str = typer.Option(
651
696
  None, "--byosnaps",
@@ -687,7 +732,7 @@ def snapend(
687
732
  api_key=ctx.obj['api_key'],
688
733
  snapend_id=snapend_id,
689
734
  # Enumerate, Clone
690
- game_id=game_id,
735
+ game_id=application_id if application_id is not None else game_id,
691
736
  # Clone
692
737
  name=name, env=env,
693
738
  # Apply, Clone
@@ -698,7 +743,7 @@ def snapend(
698
743
  category_format=category_format,
699
744
  category_type=category_type,
700
745
  category_http_lib=category_http_lib,
701
- snaps=snaps,
746
+ snaps=snaps_list_str,
702
747
  # Download, Apply and Clone
703
748
  out_path=out_path,
704
749
  # Update
@@ -759,3 +804,129 @@ def byows(
759
804
  getattr(byows_obj, subcommand.replace('-', '_'))()
760
805
  success(f"BYOWs {subcommand} complete")
761
806
  raise typer.Exit(code=SNAPCTL_SUCCESS)
807
+
808
+
809
+ @app.command()
810
+ @telemetry("snaps", subcommand_arg="subcommand")
811
+ def snaps(
812
+ ctx: typer.Context,
813
+ # Required fields
814
+ subcommand: str = typer.Argument(
815
+ ..., help="Snaps Subcommands: " + ", ".join(Snaps.SUBCOMMANDS) + "."
816
+ ),
817
+ out_path_filename: Union[str, None] = typer.Option(
818
+ None, "--out-path-filename", help=(
819
+ "(optional: enumerate) Path and filename to output the snaps list. The filename should end with .json."
820
+ )
821
+ ),
822
+ # overrides
823
+ api_key: Union[str, None] = typer.Option(
824
+ None, "--api-key", help="API Key override.", callback=api_key_context_callback
825
+ ),
826
+ profile: Union[str, None] = typer.Option(
827
+ None, "--profile", help="Profile from the Snapser config to use.", callback=profile_context_callback
828
+ ),
829
+ ) -> None:
830
+ """
831
+ Bring your own workstation commands
832
+ """
833
+ validate_command_context(ctx)
834
+ snaps_obj: Snaps = Snaps(
835
+ subcommand=subcommand,
836
+ base_url=ctx.obj['base_url'],
837
+ api_key=ctx.obj['api_key'],
838
+ out_path_filename=out_path_filename,
839
+ )
840
+ getattr(snaps_obj, subcommand.replace('-', '_'))()
841
+ success(f"Snaps {subcommand} complete")
842
+ raise typer.Exit(code=SNAPCTL_SUCCESS)
843
+
844
+
845
+ @app.command()
846
+ @telemetry("snapend_manifest", subcommand_arg="subcommand")
847
+ def snapend_manifest(
848
+ ctx: typer.Context,
849
+ # Required fields
850
+ subcommand: str = typer.Argument(
851
+ ..., help="Snapend Manifest Subcommands: " + ", ".join(SnapendManifest.SUBCOMMANDS) + "."
852
+ ),
853
+ name: Union[str, None] = typer.Option(
854
+ None, "--name", help="(req: create) Name for your snapend."
855
+ ),
856
+ env: Union[str, None] = typer.Option(
857
+ None, "--env", help=(
858
+ "(req: create) Environment for your snapend - " +
859
+ ", ".join(SnapendManifest.ENVIRONMENTS) + "."
860
+ )
861
+ ),
862
+ manifest_path_filename: Union[str, None] = typer.Option(
863
+ None, "--manifest-path-filename", help=(
864
+ "(req: sync, upgrade) Full Path to the manifest file including the filename."
865
+ )
866
+ ),
867
+ snaps_list_str: str = typer.Option(
868
+ None, "--snaps", help=(
869
+ "(use: create, sync, upgrade) Comma separated list of snap ids to add, sync or upgrade. "
870
+ )
871
+ ),
872
+ features: str = typer.Option(
873
+ None, "--features", help=(
874
+ "(use: create, sync) Comma separated list of feature flags to add, sync. "
875
+ "Features: " + ", ".join(SnapendManifest.FEATURES)
876
+ )
877
+ ),
878
+ add_snaps: str = typer.Option(
879
+ None, "--add-snaps", help=(
880
+ "(use: update) Comma separated list of snap ids to add. "
881
+ )
882
+ ),
883
+ remove_snaps: str = typer.Option(
884
+ None, "--remove-snaps", help=(
885
+ "(use: update) Comma separated list of snap ids to remove. "
886
+ )
887
+ ),
888
+ add_features: str = typer.Option(
889
+ None, "--add-features", help=(
890
+ "(use: update) Comma separated list of features to add. "
891
+ )
892
+ ),
893
+ remove_features: str = typer.Option(
894
+ None, "--remove-features", help=(
895
+ "(use: update) Comma separated list of features to remove. "
896
+ )
897
+ ),
898
+ out_path_filename: Union[str, None] = typer.Option(
899
+ None, "--out-path-filename", help=(
900
+ "(optional: enumerate) Path and filename to output the manifest. The filename should end with .json or .yaml"
901
+ )
902
+ ),
903
+ # overrides
904
+ api_key: Union[str, None] = typer.Option(
905
+ None, "--api-key", help="API Key override.", callback=api_key_context_callback
906
+ ),
907
+ profile: Union[str, None] = typer.Option(
908
+ None, "--profile", help="Profile from the Snapser config to use.", callback=profile_context_callback
909
+ ),
910
+ ) -> None:
911
+ """
912
+ Bring your own workstation commands
913
+ """
914
+ validate_command_context(ctx)
915
+ snapend_manifest_obj: SnapendManifest = SnapendManifest(
916
+ subcommand=subcommand,
917
+ base_url=ctx.obj['base_url'],
918
+ api_key=ctx.obj['api_key'],
919
+ name=name,
920
+ environment=env,
921
+ manifest_path_filename=manifest_path_filename,
922
+ snaps=snaps_list_str,
923
+ features=features,
924
+ add_snaps=add_snaps,
925
+ remove_snaps=remove_snaps,
926
+ add_features=add_features,
927
+ remove_features=remove_features,
928
+ out_path_filename=out_path_filename,
929
+ )
930
+ getattr(snapend_manifest_obj, subcommand.replace('-', '_'))()
931
+ success(f"Snapend Manifest {subcommand} complete")
932
+ raise typer.Exit(code=SNAPCTL_SUCCESS)
@@ -0,0 +1,8 @@
1
+ '''
2
+ Exceptions used in snapctl.
3
+ '''
4
+
5
+
6
+ class SnapendDownloadException(Exception):
7
+ """Raised when a Snapend download fails or is incomplete."""
8
+ pass
snapctl/utils/helper.py CHANGED
@@ -3,6 +3,7 @@ Helper functions for snapctl
3
3
  """
4
4
  from typing import Union, Dict
5
5
  from pathlib import Path
6
+ from collections import Counter
6
7
  import re
7
8
  import platform
8
9
  import os
@@ -49,7 +50,8 @@ def validate_api_key(base_url: str, api_key: Union[str, None]) -> bool:
49
50
  raise typer.Exit(code=SNAPCTL_CONFIGURATION_ERROR)
50
51
 
51
52
 
52
- def get_composite_token(base_url: str, api_key: Union[str, None], action: str, params: object) -> str:
53
+ def get_composite_token(
54
+ base_url: str, api_key: Union[str, None], action: str, params: object) -> str:
53
55
  """
54
56
  This function exchanges the api_key for a composite token.
55
57
  """
@@ -203,3 +205,10 @@ def get_config_value(environment: str, key: str) -> str:
203
205
  if environment == '' or environment not in APP_CONFIG or key not in APP_CONFIG[environment]:
204
206
  return ''
205
207
  return APP_CONFIG[environment][key]
208
+
209
+
210
+ def check_duplicates_in_list(items: list[str]) -> list[str]:
211
+ '''
212
+ Check for duplicates in a list and return the duplicate items
213
+ '''
214
+ return [k for k, v in Counter(items).items() if v > 1]