Skip to content

Commit 0271c38

Browse files
committed
Merge branch 'dev' into json_output_to_stdout
2 parents 21d7bf0 + 130cf97 commit 0271c38

File tree

10 files changed

+358
-58
lines changed

10 files changed

+358
-58
lines changed

.github/workflows/teardown.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
environment-file: environment.yml
3737
python-version: '3.12'
3838
mamba-version: '*'
39-
channels: conda-forge,bioconda,defaults
39+
channels: conda-forge,bioconda
4040
activate-environment: seqerakit
4141
use-mamba: true
4242

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ You will need to have an account on Seqera Platform (see [Plans and pricing](htt
2727
You can install `seqerakit` and its dependencies via Conda. Ensure that you have the correct channels configured:
2828

2929
```console
30-
conda config --add channels defaults
3130
conda config --add channels bioconda
3231
conda config --add channels conda-forge
3332
conda config --set channel_priority strict
@@ -170,6 +169,43 @@ seqerakit hello-world-config.yml --cli="-Djavax.net.ssl.trustStore=/absolute/pat
170169

171170
<b>Note</b>: Use of `--verbose` option for the `tw` CLI is currently not supported by `seqerakit`. Supplying `--cli="--verbose"` will raise an error.
172171

172+
## Specify targets
173+
When using a YAML file as input that defines multiple resources, you can use the `--targets` flag to specify which resources to create. This flag takes a comma-separated list of resource names.
174+
175+
For example, given a YAML file that defines the following resources:
176+
177+
```yaml
178+
workspaces:
179+
- name: 'showcase'
180+
organization: 'seqerakit_automation'
181+
...
182+
compute-envs:
183+
- name: 'compute-env'
184+
type: 'aws-batch forge'
185+
workspace: 'seqerakit/test'
186+
...
187+
pipelines:
188+
- name: "hello-world-test-seqerakit"
189+
url: "https://github.com/nextflow-io/hello"
190+
workspace: 'seqerakit/test'
191+
compute-env: "compute-env"
192+
...
193+
```
194+
195+
You can target the creation of `pipelines` only by running:
196+
197+
```bash
198+
seqerakit test.yml --targets pipelines
199+
```
200+
This will process only the pipelines block from the YAML file and ignore other blocks such as `workspaces` and `compute-envs`.
201+
202+
### Multiple Targets
203+
You can also specify multiple resources to create by separating them with commas. For example, to create both workspaces and pipelines, run:
204+
205+
```bash
206+
seqerakit test.yml --targets workspaces,pipelines
207+
```
208+
173209
## YAML Configuration Options
174210

175211
There are several options that can be provided in your YAML configuration file, that are handled specially by seqerakit and/or are not exposed as `tw` CLI options.

environment.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: seqerakitdev
2+
channels:
3+
- bioconda
4+
- conda-forge
5+
dependencies:
6+
- conda-forge::python=3.10.9
7+
- conda-forge::pyyaml=6.0
8+
- bioconda::tower-cli=0.9.2

seqerakit/cli.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def parse_args(args=None):
8383
help="Additional Seqera Platform CLI specific options to be passed,"
8484
" enclosed in double quotes (e.g. '--cli=\"--insecure\"').",
8585
)
86+
yaml_processing.add_argument(
87+
"--targets",
88+
dest="targets",
89+
type=str,
90+
help="Specify the resources to be targeted for creation in a YAML file through "
91+
"a comma-separated list (e.g. '--targets=teams,participants').",
92+
)
8693
return parser.parse_args(args)
8794

8895

@@ -146,12 +153,20 @@ def handle_block(self, block, args, destroy=False, dryrun=False):
146153

147154
def main(args=None):
148155
options = parse_args(args if args is not None else sys.argv[1:])
149-
logging.basicConfig(level=options.log_level)
156+
logging.basicConfig(level=getattr(logging, options.log_level.upper()))
157+
158+
# Parse CLI arguments into a list
159+
cli_args_list = options.cli_args.split() if options.cli_args else []
160+
161+
sp = seqeraplatform.SeqeraPlatform(
162+
cli_args=cli_args_list, dryrun=options.dryrun, json=options.json
163+
)
150164

151165
# If the info flag is set, run 'tw info'
152166
if options.info:
153-
sp = seqeraplatform.SeqeraPlatform()
154-
print(sp.info())
167+
result = sp.info()
168+
if not options.dryrun:
169+
print(result)
155170
return
156171

157172
if not options.yaml:
@@ -164,13 +179,6 @@ def main(args=None):
164179
else:
165180
options.yaml = [sys.stdin]
166181

167-
# Parse CLI arguments into a list
168-
cli_args_list = options.cli_args.split() if options.cli_args else []
169-
170-
sp = seqeraplatform.SeqeraPlatform(
171-
cli_args=cli_args_list, dryrun=options.dryrun, json=options.json
172-
)
173-
174182
block_manager = BlockParser(
175183
sp,
176184
[
@@ -188,18 +196,15 @@ def main(args=None):
188196
# Parse the YAML file(s) by blocks
189197
# and get a dictionary of command line arguments
190198
try:
191-
cmd_args_dict = helper.parse_all_yaml(options.yaml, destroy=options.delete)
199+
cmd_args_dict = helper.parse_all_yaml(
200+
options.yaml, destroy=options.delete, targets=options.targets
201+
)
192202
for block, args_list in cmd_args_dict.items():
193203
for args in args_list:
194-
try:
195-
# Run the 'tw' methods for each block
196-
block_manager.handle_block(
197-
block, args, destroy=options.delete, dryrun=options.dryrun
198-
)
199-
except (ResourceExistsError, ResourceCreationError) as e:
200-
logging.error(e)
201-
sys.exit(1)
202-
except ValueError as e:
204+
block_manager.handle_block(
205+
block, args, destroy=options.delete, dryrun=options.dryrun
206+
)
207+
except (ResourceExistsError, ResourceCreationError, ValueError) as e:
203208
logging.error(e)
204209
sys.exit(1)
205210

seqerakit/helper.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def parse_yaml_block(yaml_data, block_name):
5555
return block_name, cmd_args_list
5656

5757

58-
def parse_all_yaml(file_paths, destroy=False):
58+
def parse_all_yaml(file_paths, destroy=False, targets=None):
5959
# If multiple yamls, merge them into one dictionary
6060
merged_data = {}
6161

@@ -108,6 +108,11 @@ def parse_all_yaml(file_paths, destroy=False):
108108

109109
block_names = list(merged_data.keys())
110110

111+
# Filter blocks based on targets if provided
112+
if targets:
113+
target_blocks = set(targets.split(","))
114+
block_names = [block for block in block_names if block in target_blocks]
115+
111116
# Define the order in which the resources should be created.
112117
resource_order = [
113118
"organizations",

seqerakit/overwrite.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ def handle_overwrite(self, block, args, overwrite=False, destroy=False):
117117
if self.check_resource_exists(operation["name_key"], sp_args):
118118
# if resource exists and overwrite is true, delete
119119
if overwrite:
120-
logging.debug(
120+
logging.info(
121121
f" The attempted {block} resource already exists."
122122
" Overwriting.\n"
123123
)
124124
self.delete_resource(block, operation, sp_args)
125125
elif destroy:
126-
logging.debug(f" Deleting the {block} resource.")
126+
logging.info(f" Deleting the {block} resource.")
127127
self.delete_resource(block, operation, sp_args)
128128
else: # return an error if resource exists, overwrite=False
129129
raise ResourceExistsError(
@@ -147,7 +147,8 @@ def _get_team_args(self, args):
147147

148148
if not jsondata:
149149
json_method = getattr(self.sp, "-o json")
150-
json_out = json_method("teams", "list", "-o", args["organization"])
150+
with self.sp.suppress_output():
151+
json_out = json_method("teams", "list", "-o", args["organization"])
151152
self.block_jsondata["teams"] = json_out
152153
else:
153154
json_out = jsondata
@@ -244,27 +245,30 @@ def _get_json_data(self, block, args, keys_to_get):
244245
# Fetch the data if it does not exist
245246
if block == "teams":
246247
sp_args = self._get_values_from_cmd_args(args[0], keys_to_get)
247-
self.cached_jsondata = json_method(
248-
block, "list", "-o", sp_args["organization"]
249-
)
248+
with self.sp.suppress_output():
249+
self.cached_jsondata = json_method(
250+
block, "list", "-o", sp_args["organization"]
251+
)
250252
elif block in Overwrite.generic_deletion or block in {
251253
"participants",
252254
"labels",
253255
}:
254256
sp_args = self._get_values_from_cmd_args(args, keys_to_get)
255-
self.cached_jsondata = json_method(
256-
block, "list", "-w", sp_args["workspace"]
257-
)
258-
elif block == "members" or block == "workspaces": # TODO
257+
with self.sp.suppress_output():
258+
self.cached_jsondata = json_method(
259+
block, "list", "-w", sp_args["workspace"]
260+
)
261+
elif block == "members" or block == "workspaces":
259262
sp_args = self._get_values_from_cmd_args(args, keys_to_get)
260-
self.cached_jsondata = json_method(
261-
block, "list", "-o", sp_args["organization"]
262-
)
263+
with self.sp.suppress_output():
264+
self.cached_jsondata = json_method(
265+
block, "list", "-o", sp_args["organization"]
266+
)
263267
else:
264268
sp_args = self._get_values_from_cmd_args(args, keys_to_get)
265-
self.cached_jsondata = json_method(block, "list")
269+
with self.sp.suppress_output():
270+
self.cached_jsondata = json_method(block, "list")
266271

267-
# Store this data in the block_jsondata dict for later use
268272
self.block_jsondata[block] = self.cached_jsondata
269273
return self.cached_jsondata, sp_args
270274

seqerakit/seqeraplatform.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from contextlib import contextmanager
1516
import os
1617
import shlex
1718
import logging
1819
import subprocess
1920
import re
2021
import json
2122

22-
logging.basicConfig(level=logging.DEBUG)
23-
2423

2524
class SeqeraPlatform:
2625
"""
@@ -44,14 +43,16 @@ def __call__(self, *args, **kwargs):
4443
return self.tw_instance._tw_run(command, **kwargs)
4544

4645
# Constructs a new SeqeraPlatform instance
47-
def __init__(self, cli_args=None, dryrun=False, json=False):
46+
def __init__(self, cli_args=None, dryrun=False, print_stdout=True, json=False):
4847
if cli_args and "--verbose" in cli_args:
4948
raise ValueError(
5049
"--verbose is not supported as a CLI argument to seqerakit."
5150
)
5251
self.cli_args = cli_args or []
5352
self.dryrun = dryrun
53+
self.print_stdout = print_stdout
5454
self.json = json
55+
self._suppress_output = False
5556

5657
def _construct_command(self, cmd, *args, **kwargs):
5758
command = ["tw"] + self.cli_args
@@ -68,8 +69,19 @@ def _construct_command(self, cmd, *args, **kwargs):
6869
if "params_file" in kwargs:
6970
command.append(f"--params-file={kwargs['params_file']}")
7071

72+
# Check for empty string arguments and handle them
73+
self._check_empty_args(command)
74+
7175
return self._check_env_vars(command)
7276

77+
def _check_empty_args(self, command):
78+
for current_arg, next_arg in zip(command, command[1:]):
79+
if isinstance(next_arg, str) and next_arg.strip() == "":
80+
raise ValueError(
81+
f"Empty string argument found for parameter '{current_arg}'. "
82+
"Please provide a valid value or remove the argument."
83+
)
84+
7385
# Checks environment variables to see that they are set accordingly
7486
def _check_env_vars(self, command):
7587
full_cmd_parts = []
@@ -89,16 +101,23 @@ def _check_env_vars(self, command):
89101
return " ".join(full_cmd_parts)
90102

91103
# Executes a 'tw' command in a subprocess and returns the output.
92-
def _execute_command(self, full_cmd):
93-
logging.debug(f" Running command: {full_cmd}")
104+
def _execute_command(self, full_cmd, to_json=False, print_stdout=True):
105+
logging.info(f" Running command: {full_cmd}")
94106
process = subprocess.Popen(
95107
full_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True
96108
)
97109
stdout, _ = process.communicate()
98110
stdout = stdout.decode("utf-8").strip()
99111

112+
should_print = (
113+
print_stdout if print_stdout is not None else self.print_stdout
114+
) and not self._suppress_output
115+
116+
if should_print:
117+
logging.info(f" Command output: {stdout}")
118+
100119
if "ERROR: " in stdout or process.returncode != 0:
101-
self._handle_command_errors(str(stdout))
120+
self._handle_command_errors(stdout)
102121

103122
if self.json:
104123
out = json.loads(stdout)
@@ -109,14 +128,7 @@ def _execute_command(self, full_cmd):
109128

110129
return out
111130

112-
def _execute_info_command(self):
113-
# Directly execute 'tw info' command
114-
command = "tw info"
115-
return self._execute_command(command)
116-
117131
def _handle_command_errors(self, stdout):
118-
logging.error(stdout)
119-
120132
# Check for specific tw cli error patterns and raise custom exceptions
121133
if re.search(
122134
r"ERROR: .*already (exists|a participant)", stdout, flags=re.IGNORECASE
@@ -131,19 +143,33 @@ def _handle_command_errors(self, stdout):
131143
)
132144

133145
def _tw_run(self, cmd, *args, **kwargs):
146+
print_stdout = kwargs.pop("print_stdout", None)
134147
full_cmd = self._construct_command(cmd, *args, **kwargs)
135148
if not full_cmd or self.dryrun:
136-
logging.debug(f"DRYRUN: Running command {full_cmd}")
149+
logging.info(f"DRYRUN: Running command {full_cmd}")
137150
return None
138-
result = self._execute_command(full_cmd)
139-
return result
151+
return self._execute_command(full_cmd, kwargs.get("to_json"), print_stdout)
152+
153+
@contextmanager
154+
def suppress_output(self):
155+
original_suppress = self._suppress_output
156+
self._suppress_output = True
157+
try:
158+
yield
159+
finally:
160+
self._suppress_output = original_suppress
140161

141162
# Allow any 'tw' subcommand to be called as a method.
142163
def __getattr__(self, cmd):
143164
if cmd == "info":
144-
return self._execute_info_command
145-
else:
146-
return self.TwCommand(self, cmd.replace("_", "-"))
165+
return lambda *args, **kwargs: self._tw_run(
166+
["info"], *args, **kwargs, print_stdout=False
167+
)
168+
if cmd == "-o json":
169+
return lambda *args, **kwargs: self._tw_run(
170+
["-o", "json"] + list(args), **kwargs
171+
)
172+
return self.TwCommand(self, cmd.replace("_", "-"))
147173

148174

149175
class ResourceExistsError(Exception):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from setuptools import find_packages, setup
44

5-
VERSION = "0.4.7"
5+
VERSION = "0.4.9"
66

77
with open("README.md") as f:
88
readme = f.read()

0 commit comments

Comments
 (0)