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

Clinical-Genomics / microSALT / #265

30 Oct 2024 01:20PM UTC coverage: 76.105% (-6.0%) from 82.069%
#265

push

travis-ci

web-flow
Merge pull request #178 from Clinical-Genomics/rc400 (major)

### Added 

- Release candidate microSALT 4.0.0

8 of 16 new or added lines in 4 files covered. (50.0%)

260 existing lines in 5 files now uncovered.

1997 of 2624 relevant lines covered (76.11%)

0.76 hits per line

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

73.57
/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 click
1✔
7
import json
1✔
8
import os
1✔
9
import re
1✔
10
import subprocess
1✔
11
import sys
1✔
12
import yaml
1✔
13

14
from pkg_resources import iter_entry_points
1✔
15
from microSALT import __version__, preset_config, logger, wd
1✔
16
from microSALT.utils.scraper import Scraper
1✔
17
from microSALT.utils.job_creator import Job_Creator
1✔
18
from microSALT.utils.reporter import Reporter
1✔
19
from microSALT.utils.referencer import Referencer
1✔
20

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

38

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

45

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

60

61
def done():
1✔
62
    click.echo("INFO - Execution finished!")
1✔
63
    logger.debug("INFO - Execution finished!")
1✔
64

65

66
def review_sampleinfo(pfile):
1✔
67
    """Reviews sample info. Returns loaded json object"""
68

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

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

95

96
@click.group()
1✔
97
@click.version_option(__version__)
1✔
98
@click.pass_context
1✔
99
def root(ctx):
100
    """microbial Sequence Analysis and Loci-based Typing (microSALT) pipeline"""
101
    ctx.obj = {}
1✔
102
    ctx.obj["config"] = preset_config
1✔
103
    ctx.obj["log"] = logger
1✔
104

105

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

UNCOV
159
    run_settings = {
×
160
        "input": input,
161
        "dry": dry,
162
        "email": email,
163
        "skip_update": skip_update,
164
        "trimmed": not untrimmed,
165
        "pool": pool,
166
    }
167

168
    # Samples section
UNCOV
169
    sampleinfo = review_sampleinfo(sampleinfo_file)
×
UNCOV
170
    run_creator = Job_Creator(
×
171
        config=ctx.obj["config"],
172
        log=ctx.obj["log"],
173
        sampleinfo=sampleinfo,
174
        run_settings=run_settings,
175
    )
176

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

UNCOV
200
    done()
×
201

202

203
@root.group()
1✔
204
@click.pass_context
1✔
205
def utils(ctx):
206
    """Utilities for specific purposes"""
207
    pass
1✔
208

209

210
@utils.group()
1✔
211
@click.pass_context
1✔
212
def refer(ctx):
213
    """Manipulates MLST organisms"""
214
    pass
1✔
215

216

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

268
    run_settings = {
1✔
269
        "input": input,
270
        "track": track,
271
        "dry": dry,
272
        "email": email,
273
        "skip_update": skip_update,
274
    }
275

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

292
    res_scraper = Scraper(
1✔
293
        config=ctx.obj["config"], log=ctx.obj["log"], sampleinfo=sampleinfo, input=input
294
    )
295
    if isinstance(sampleinfo, list) and len(sampleinfo) > 1:
1✔
296
        res_scraper.scrape_project()
1✔
297
        # for subfolder in pool:
298
        #  res_scraper.scrape_sample()
299
    else:
300
        res_scraper.scrape_sample()
×
301

302
    codemonkey = Reporter(
1✔
303
        config=ctx.obj["config"],
304
        log=ctx.obj["log"],
305
        sampleinfo=sampleinfo,
306
        output=output,
307
        collection=True,
308
    )
309
    codemonkey.report(report)
1✔
310
    done()
1✔
311

312

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

331

332
@refer.command()
1✔
333
@click.pass_context
1✔
334
def observe(ctx):
335
    """Lists all stored organisms"""
336
    refe = Referencer(config=ctx.obj["config"], log=ctx.obj["log"])
1✔
337
    click.echo("INFO - Currently stored organisms:")
1✔
338
    for org in sorted(refe.existing_organisms()):
1✔
339
        click.echo(org.replace("_", " ").capitalize())
1✔
340

341

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

373

374
@utils.command()
1✔
375
@click.pass_context
1✔
376
def view(ctx):
377
    """Starts an interactive webserver for viewing"""
378
    codemonkey = Reporter(config=ctx.obj["config"], log=ctx.obj["log"])
1✔
379
    codemonkey.start_web()
1✔
380

381

382
@utils.command()
1✔
383
@click.option("--input", help="Full path to project folder", default=os.getcwd())
1✔
384
@click.pass_context
1✔
385
def generate(ctx, input):
386
    """Creates a blank sample info json for the given input folder"""
387
    input = os.path.abspath(input)
1✔
388
    project_name = os.path.basename(input)
1✔
389

390
    defaults = default_sampleinfo.copy()
1✔
391

392
    pool = []
1✔
393
    if not os.path.isdir(input):
1✔
394
        click.echo(
×
395
            "ERROR - Sequence data folder {} does not exist.".format(project_name)
396
        )
397
        ctx.abort()
×
398
    elif input != os.getcwd():
1✔
399
        for subfolder in os.listdir(input):
1✔
400
            if os.path.isdir("{}/{}".format(input, subfolder)):
1✔
401
                pool.append(defaults.copy())
1✔
402
                pool[-1]["CG_ID_project"] = project_name
1✔
403
                pool[-1]["CG_ID_sample"] = subfolder
1✔
404
    else:
405
        project_name = "default_sample_info"
1✔
406
        pool.append(defaults.copy())
1✔
407

408
    with open("{}/{}.json".format(os.getcwd(), project_name), "w") as output:
1✔
409
        json.dump(pool, output, indent=2)
1✔
410
    click.echo("INFO - Created {}.json in current folder".format(project_name))
1✔
411
    done()
1✔
412

413

414
@utils.group()
1✔
415
@click.pass_context
1✔
416
def resync(ctx):
417
    """Updates internal ST with pubMLST equivalent"""
418

419

420
@resync.command()
1✔
421
@click.option(
1✔
422
    "--type",
423
    default="list",
424
    type=click.Choice(["report", "list"]),
425
    help="Output format",
426
)
427
@click.option("--customer", default="all", help="Customer id filter")
1✔
428
@click.option(
1✔
429
    "--skip_update", default=False, help="Skips downloading of references", is_flag=True
430
)
431
@click.option(
1✔
432
    "--email",
433
    default=preset_config["regex"]["mail_recipient"],
434
    help="Forced e-mail recipient",
435
)
436
@click.option("--output", help="Full path to output folder", default="")
1✔
437
@click.pass_context
1✔
438
def review(ctx, type, customer, skip_update, email, output):
439
    """Generates information about novel ST"""
440
    # Trace exists by some samples having pubMLST_ST filled in. Make trace function later
441
    ctx.obj["config"]["regex"]["mail_recipient"] = email
1✔
442
    ext_refs = Referencer(config=ctx.obj["config"], log=ctx.obj["log"])
1✔
443
    if not skip_update:
1✔
444
        ext_refs.update_refs()
1✔
445
        ext_refs.resync()
1✔
446
    click.echo("INFO - Version check done. Generating output")
1✔
447
    if type == "report":
1✔
448
        codemonkey = Reporter(
1✔
449
            config=ctx.obj["config"], log=ctx.obj["log"], output=output
450
        )
451
        codemonkey.report(type="st_update", customer=customer)
1✔
452
    elif type == "list":
1✔
453
        ext_refs.resync(type=type)
1✔
454
    done()
1✔
455

456

457
@resync.command()
1✔
458
@click.argument("sample_name")
1✔
459
@click.option(
1✔
460
    "--force",
461
    default=False,
462
    is_flag=True,
463
    help="Resolves sample without checking for pubMLST match",
464
)
465
@click.pass_context
1✔
466
def overwrite(ctx, sample_name, force):
467
    """Flags sample as resolved"""
468
    ext_refs = Referencer(config=ctx.obj["config"], log=ctx.obj["log"])
1✔
469
    ext_refs.resync(type="overwrite", sample=sample_name, ignore=force)
1✔
470
    done()
1✔
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