• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Clinical-Genomics / microSALT / #362

05 Jun 2025 08:56AM UTC coverage: 40.487% (-0.03%) from 40.513%
#362

push

travis-ci

1296 of 3201 relevant lines covered (40.49%)

0.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.72
/microSALT/cli.py
1
"""This is the main entry point of microSALT.
2
By: Isak Sylvin, @sylvinite"""
3

4
#!/usr/bin/env python
5

6
import json
1✔
7
import logging
1✔
8
import os
1✔
9
import sys
1✔
10

1✔
11
import click
1✔
12

1✔
13
from microSALT import __version__, logging_levels, preset_config
1✔
14
from microSALT.utils.job_creator import Job_Creator
15
from microSALT.utils.referencer import Referencer
1✔
16
from microSALT.utils.reporter import Reporter
1✔
17
from microSALT.utils.scraper import Scraper
1✔
18

1✔
19
default_sampleinfo = {
1✔
20
    "CG_ID_project": "XXX0000",
1✔
21
    "CG_ID_sample": "XXX0000A1",
22
    "Customer_ID_project": "100100",
1✔
23
    "Customer_ID_sample": "10XY123456",
24
    "Customer_ID": "cust000",
25
    "application_tag": "SOMTIN100",
26
    "date_arrival": "0001-01-01 00:00:00",
27
    "date_libprep": "0001-01-01 00:00:00",
28
    "date_sequencing": "0001-01-01 00:00:00",
29
    "method_libprep": "Not in LIMS",
30
    "method_sequencing": "Not in LIMS",
31
    "organism": "Staphylococcus aureus",
32
    "priority": "standard",
33
    "reference": "None",
34
}
35

36
logger = logging.getLogger("main_logger")
37

38
if preset_config == "":
39
    click.echo(
1✔
40
        "ERROR - No properly set-up config under neither envvar MICROSALT_CONFIG nor ~/.microSALT/config.json. Exiting."
41
    )
1✔
42
    sys.exit(-1)
×
43

44

45
@click.pass_context
×
46
def set_cli_config(ctx, config):
47
    if config != "":
48
        if os.path.exists(config):
1✔
49
            try:
50
                t = ctx.obj["config"]
×
51
                with open(os.path.abspath(config), "r") as conf:
×
52
                    ctx.obj["config"] = json.load(conf)
×
53
                ctx.obj["config"]["folders"]["expec"] = t["folders"]["expec"]
×
54
                ctx.obj["config"]["folders"]["adapters"] = t["folders"]["adapters"]
×
55
                ctx.obj["config"]["config_path"] = os.path.abspath(config)
×
56
            except Exception:
×
57
                pass
×
58

×
59

×
60
def done():
×
61
    click.echo("INFO - Execution finished!")
62
    logger.debug("INFO - Execution finished!")
63

1✔
64

×
65
def review_sampleinfo(pfile):
×
66
    """Reviews sample info. Returns loaded json object"""
67

68
    try:
1✔
69
        with open(pfile) as json_file:
70
            data = json.load(json_file)
71
    except Exception:
×
72
        click.echo("Unable to read provided sample info file as json. Exiting..")
×
73
        sys.exit(-1)
×
74

×
75
    if isinstance(data, list):
×
76
        for entry in data:
×
77
            for k, v in default_sampleinfo.items():
78
                if k not in entry:
×
79
                    click.echo(
×
80
                        "WARNING - Parameter {} needs to be provided in sample json. Formatting example: ({})".format(
×
81
                            k, v
×
82
                        )
×
83
                    )
84
    else:
85
        for k, v in default_sampleinfo.items():
86
            if k not in data:
87
                click.echo(
88
                    "WARNING - Parameter {} needs to be provided in sample json. Formatting example: ({})".format(
×
89
                        k, v
×
90
                    )
×
91
                )
92
    return data
93

94

95
@click.group()
×
96
@click.version_option(__version__)
97
@click.option(
98
    "--logging-level",
1✔
99
    default="INFO",
1✔
100
    type=click.Choice(list(logging_levels.keys())),
1✔
101
    help="Set the logging level for the CLI",
102
)
103
@click.pass_context
104
def root(ctx, logging_level):
105
    """microbial Sequence Analysis and Loci-based Typing (microSALT) pipeline"""
106
    ctx.obj = {}
1✔
107
    ctx.obj["config"] = preset_config
108
    logger.setLevel(logging_levels[logging_level])
109
    for handler in logger.handlers:
×
110
        handler.setLevel(logging_levels[logging_level])
×
111
    logger.debug(f"Setting logging level to {logging_levels[logging_level]}")
×
112

×
113

×
114
@root.command()
×
115
@click.argument("sampleinfo_file")
116
@click.option("--input", help="Full path to input folder", default="")
117
@click.option("--config", help="microSALT config to override default", default="")
1✔
118
@click.option(
1✔
119
    "--dry",
1✔
120
    help="Builds instance without posting to SLURM",
1✔
121
    default=False,
1✔
122
    is_flag=True,
123
)
124
@click.option(
125
    "--email",
126
    default=preset_config["regex"]["mail_recipient"],
127
    help="Forced e-mail recipient",
1✔
128
)
129
@click.option("--skip_update", default=False, help="Skips downloading of references", is_flag=True)
130
@click.option(
131
    "--force_update",
132
    default=False,
1✔
133
    help="Forces downloading of pubMLST references",
1✔
134
    is_flag=True,
135
)
136
@click.option("--untrimmed", help="Use untrimmed input data", default=False, is_flag=True)
137
@click.pass_context
138
def analyse(
139
    ctx,
1✔
140
    sampleinfo_file,
1✔
141
    input,
142
    config,
143
    dry,
144
    email,
145
    skip_update,
146
    force_update,
147
    untrimmed,
148
):
149
    """Sequence analysis, typing and resistance identification"""
150
    # Run section
151
    pool = []
152
    trimmed = not untrimmed
153
    set_cli_config(config)
154
    ctx.obj["config"]["regex"]["mail_recipient"] = email
×
155
    ctx.obj["config"]["dry"] = dry
×
156
    if not os.path.isdir(input):
×
157
        click.echo("ERROR - Sequence data folder {} does not exist.".format(input))
×
158
        ctx.abort()
×
159
    for subfolder in os.listdir(input):
×
160
        if os.path.isdir("{}/{}".format(input, subfolder)):
×
161
            pool.append(subfolder)
×
162

×
163
    run_settings = {
×
164
        "input": input,
×
165
        "dry": dry,
166
        "email": email,
×
167
        "skip_update": skip_update,
168
        "trimmed": not untrimmed,
169
        "pool": pool,
170
    }
171

172
    # Samples section
173
    sampleinfo = review_sampleinfo(sampleinfo_file)
174
    run_creator = Job_Creator(
175
        config=ctx.obj["config"],
176
        log=logger,
×
177
        sampleinfo=sampleinfo,
×
178
        run_settings=run_settings,
179
    )
180

181
    ext_refs = Referencer(
182
        config=ctx.obj["config"],
183
        log=logger,
184
        sampleinfo=sampleinfo,
×
185
        force=force_update,
186
    )
187
    click.echo("INFO - Checking versions of references..")
188
    try:
189
        if not skip_update:
190
            ext_refs.identify_new(project=True)
×
191
            ext_refs.update_refs()
×
192
            click.echo("INFO - Version check done. Creating sbatch jobs")
×
193
        else:
×
194
            click.echo("INFO - Skipping version check.")
×
195
    except Exception as e:
×
196
        click.echo("{}".format(e))
197
    if len(sampleinfo) > 1:
×
198
        run_creator.project_job()
×
199
    elif len(sampleinfo) == 1:
×
200
        run_creator.project_job(single_sample=True)
×
201
    else:
×
202
        ctx.abort()
×
203

×
204
    done()
205

×
206

207
@root.group()
×
208
@click.pass_context
209
def utils(ctx):
210
    """Utilities for specific purposes"""
1✔
211
    pass
1✔
212

213

214
@utils.group()
×
215
@click.pass_context
216
def refer(ctx):
217
    """Manipulates MLST organisms"""
1✔
218
    pass
1✔
219

220

221
@utils.command()
×
222
@click.argument("sampleinfo_file")
223
@click.option("--input", help="Full path to project folder", default="")
224
@click.option(
1✔
225
    "--track",
1✔
226
    help="Run a specific analysis track",
1✔
227
    default="default",
1✔
228
    type=click.Choice(["default", "typing", "qc", "cgmlst"]),
229
)
230
@click.option("--config", help="microSALT config to override default", default="")
231
@click.option(
232
    "--dry",
233
    help="Builds instance without posting to SLURM",
1✔
234
    default=False,
1✔
235
    is_flag=True,
236
)
237
@click.option(
238
    "--email",
239
    default=preset_config["regex"]["mail_recipient"],
240
    help="Forced e-mail recipient",
1✔
241
)
242
@click.option("--skip_update", default=False, help="Skips downloading of references", is_flag=True)
243
@click.option(
244
    "--report",
245
    default="default",
1✔
246
    type=click.Choice(["default", "typing", "motif_overview", "qc", "json_dump", "st_update"]),
1✔
247
)
248
@click.option("--output", help="Report output folder", default="")
249
@click.pass_context
250
def finish(ctx, sampleinfo_file, input, track, config, dry, email, skip_update, report, output):
251
    """Sequence analysis, typing and resistance identification"""
1✔
252
    # Run section
1✔
253
    pool = []
254
    set_cli_config(config)
255
    ctx.obj["config"]["regex"]["mail_recipient"] = email
256
    ctx.obj["config"]["dry"] = dry
×
257
    if not os.path.isdir(input):
×
258
        click.echo("ERROR - Sequence data folder {} does not exist.".format(input))
×
259
        ctx.abort()
×
260
    if output == "":
×
261
        output = input
×
262
    for subfolder in os.listdir(input):
×
263
        if os.path.isdir("{}/{}".format(input, subfolder)):
×
264
            pool.append(subfolder)
×
265

×
266
    run_settings = {
×
267
        "input": input,
×
268
        "track": track,
269
        "dry": dry,
×
270
        "email": email,
271
        "skip_update": skip_update,
272
    }
273

274
    # Samples section
275
    sampleinfo = review_sampleinfo(sampleinfo_file)
276
    ext_refs = Referencer(config=ctx.obj["config"], log=logger, sampleinfo=sampleinfo)
277
    click.echo("INFO - Checking versions of references..")
278
    try:
×
279
        if not skip_update:
×
280
            ext_refs.identify_new(project=True)
×
281
            ext_refs.update_refs()
×
282
            click.echo("INFO - Version check done. Creating sbatch jobs")
×
283
        else:
×
284
            click.echo("INFO - Skipping version check.")
×
285
    except Exception as e:
×
286
        click.echo("{}".format(e))
287

×
288
    res_scraper = Scraper(config=ctx.obj["config"], log=logger, sampleinfo=sampleinfo, input=input)
×
289
    if isinstance(sampleinfo, list) and len(sampleinfo) > 1:
×
290
        res_scraper.scrape_project()
291
        # for subfolder in pool:
×
292
        #  res_scraper.scrape_sample()
×
293
    else:
×
294
        res_scraper.scrape_sample()
295

296
    codemonkey = Reporter(
297
        config=ctx.obj["config"],
×
298
        log=logger,
299
        sampleinfo=sampleinfo,
×
300
        output=output,
301
        collection=True,
302
    )
303
    codemonkey.report(report)
304
    done()
305

306

×
307
@refer.command()
×
308
@click.argument("organism")
309
@click.option("--force", help="Redownloads existing organism", default=False, is_flag=True)
310
@click.pass_context
1✔
311
def add(ctx, organism, force):
1✔
312
    """Adds a new internal organism from pubMLST"""
1✔
313
    referee = Referencer(config=ctx.obj["config"], log=logger, force=force)
1✔
314
    try:
315
        referee.add_pubmlst(organism)
316
    except Exception as e:
×
317
        click.echo(e.args[0])
×
318
        ctx.abort()
×
319
    click.echo("INFO - Checking versions of all references..")
×
320
    referee = Referencer(config=ctx.obj["config"], log=logger, force=force)
×
321
    referee.update_refs()
×
322

×
323

×
324
@refer.command()
×
325
@click.pass_context
326
def observe(ctx):
327
    """Lists all stored organisms"""
1✔
328
    refe = Referencer(config=ctx.obj["config"], log=logger)
1✔
329
    click.echo("INFO - Currently stored organisms:")
330
    for org in sorted(refe.existing_organisms()):
331
        click.echo(org.replace("_", " ").capitalize())
×
332

×
333

×
334
@utils.command()
×
335
@click.argument("sampleinfo_file")
336
@click.option(
337
    "--email",
1✔
338
    default=preset_config["regex"]["mail_recipient"],
1✔
339
    help="Forced e-mail recipient",
1✔
340
)
341
@click.option(
342
    "--type",
343
    default="default",
344
    type=click.Choice(["default", "typing", "motif_overview", "qc", "json_dump", "st_update"]),
1✔
345
)
346
@click.option("--output", help="Full path to output folder", default="")
347
@click.option("--collection", default=False, is_flag=True)
348
@click.pass_context
349
def report(ctx, sampleinfo_file, email, type, output, collection):
1✔
350
    """Re-generates report for a project"""
1✔
351
    ctx.obj["config"]["regex"]["mail_recipient"] = email
1✔
352
    sampleinfo = review_sampleinfo(sampleinfo_file)
353
    codemonkey = Reporter(
354
        config=ctx.obj["config"],
×
355
        log=logger,
×
356
        sampleinfo=sampleinfo,
×
357
        output=output,
358
        collection=collection,
359
    )
360
    codemonkey.report(type)
361
    done()
362

363

×
364
@utils.command()
×
365
@click.pass_context
366
def view(ctx):
367
    """Starts an interactive webserver for viewing"""
1✔
368
    codemonkey = Reporter(config=ctx.obj["config"], log=logger)
1✔
369
    codemonkey.start_web()
370

371

×
372
@utils.command()
×
373
@click.option("--input", help="Full path to project folder", default=os.getcwd())
374
@click.pass_context
375
def generate(ctx, input):
1✔
376
    """Creates a blank sample info json for the given input folder"""
1✔
377
    input = os.path.abspath(input)
1✔
378
    project_name = os.path.basename(input)
379

380
    defaults = default_sampleinfo.copy()
×
381

×
382
    pool = []
383
    if not os.path.isdir(input):
×
384
        click.echo("ERROR - Sequence data folder {} does not exist.".format(project_name))
385
        ctx.abort()
×
386
    elif input != os.getcwd():
×
387
        for subfolder in os.listdir(input):
×
388
            if os.path.isdir("{}/{}".format(input, subfolder)):
×
389
                pool.append(defaults.copy())
×
390
                pool[-1]["CG_ID_project"] = project_name
×
391
                pool[-1]["CG_ID_sample"] = subfolder
×
392
    else:
×
393
        project_name = "default_sample_info"
×
394
        pool.append(defaults.copy())
×
395

396
    with open("{}/{}.json".format(os.getcwd(), project_name), "w") as output:
×
397
        json.dump(pool, output, indent=2)
×
398
    click.echo("INFO - Created {}.json in current folder".format(project_name))
399
    done()
×
400

×
401

×
402
@utils.group()
×
403
@click.pass_context
404
def resync(ctx):
405
    """Updates internal ST with pubMLST equivalent"""
1✔
406

1✔
407

408
@resync.command()
409
@click.option(
410
    "--type",
411
    default="list",
1✔
412
    type=click.Choice(["report", "list"]),
1✔
413
    help="Output format",
414
)
415
@click.option("--customer", default="all", help="Customer id filter")
416
@click.option("--skip_update", default=False, help="Skips downloading of references", is_flag=True)
417
@click.option(
418
    "--email",
1✔
419
    default=preset_config["regex"]["mail_recipient"],
1✔
420
    help="Forced e-mail recipient",
1✔
421
)
422
@click.option("--output", help="Full path to output folder", default="")
423
@click.pass_context
424
def review(ctx, type, customer, skip_update, email, output):
425
    """Generates information about novel ST"""
1✔
426
    # Trace exists by some samples having pubMLST_ST filled in. Make trace function later
1✔
427
    ctx.obj["config"]["regex"]["mail_recipient"] = email
428
    ext_refs = Referencer(config=ctx.obj["config"], log=logger)
429
    if not skip_update:
430
        ext_refs.update_refs()
×
431
        ext_refs.resync()
×
432
    click.echo("INFO - Version check done. Generating output")
×
433
    if type == "report":
×
434
        codemonkey = Reporter(config=ctx.obj["config"], log=logger, output=output)
×
435
        codemonkey.report(type="st_update", customer=customer)
×
436
    elif type == "list":
×
437
        ext_refs.resync(type=type)
×
438
    done()
×
439

×
440

×
441
@resync.command()
×
442
@click.option("--force-update", default=False, is_flag=True, help="Forces update")
443
@click.pass_context
444
def update_refs(ctx, force_update: bool):
1✔
445
    """Updates all references"""
1✔
446
    ext_refs = Referencer(config=ctx.obj["config"], log=logger, force=force_update)
1✔
447
    ext_refs.update_refs()
1✔
448
    done()
449

×
450

×
451
@resync.command()
×
452
@click.option("--force-update", default=False, is_flag=True, help="Forces update")
453
@click.pass_context
454
def update_from_static(ctx, force_update: bool):
1✔
455
    """Updates a specific organism"""
1✔
456
    ext_refs = Referencer(config=ctx.obj["config"], force=force_update, log=logger)
1✔
457
    ext_refs.fetch_external()
1✔
458
    done()
459

×
460

×
461
@resync.command()
×
462
@click.argument("organism")
463
@click.option("--force-update", default=False, is_flag=True, help="Forces update")
464
@click.option("--external", is_flag=True, default=False, help="Updates from external sources")
1✔
465
@click.pass_context
1✔
466
def update_organism(ctx, external: bool, force_update: bool, organism: str):
1✔
467
    """Updates a specific organism"""
1✔
468
    ext_refs = Referencer(config=ctx.obj["config"], log=logger, force=force_update)
1✔
469
    ext_refs.update_organism(external=external, organism=organism)
1✔
470
    done()
471

×
472

×
473
@resync.command()
×
474
@click.argument("sample_name")
475
@click.option(
476
    "--force",
1✔
477
    default=False,
1✔
478
    is_flag=True,
1✔
479
    help="Resolves sample without checking for pubMLST match",
480
)
481
@click.pass_context
482
def overwrite(ctx, sample_name, force):
483
    """Flags sample as resolved"""
484
    ext_refs = Referencer(config=ctx.obj["config"], log=logger)
1✔
485
    ext_refs.resync(type="overwrite", sample=sample_name, ignore=force)
486
    done()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc