Clui - Snailz Docs

Skip to content

# Clui

Command-line interface for snailz.

Each subcommand takes options --output (output file path), --params (parameter file), and --seed (random number seed) along with command-specific parameters. If a parameter file is given, it is read first and additional parameters override its values. If a parameter file is not given, all other parameters are required.

## `cli()`

Command-line interface for snailz.

Source code in `src/snailz/clui.py`

    38
    39
    40
    @click.group()
    def cli():
        """Command-line interface for snailz."""

## `all(params, output)`

Generate all data files.

Source code in `src/snailz/clui.py`

     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    @cli.command()
    @click.option(
        "--params",
        type=click.Path(exists=True, file_okay=False, dir_okay=True),
        required=True,
        help="Directory containing parameter files",
    )
    @click.option(
        "--output",
        type=click.Path(file_okay=False, dir_okay=True),
        required=True,
        help="Directory for output files",
    )
    def all(params, output):
        """Generate all data files."""
        try:
            # Check that all required parameter files exist
            params_dir = Path(params)
            required_files = ["grid.json", "people.json", "specimens.json", "assays.json"]
            for filename in required_files:
                if not (params_dir / filename).exists():
                    utils.fail(
                        f"Required parameter file {filename} not found in {params_dir}"
                    )

            # Prepare output directory
            output_dir = Path(output)
            if output_dir.exists():
                shutil.rmtree(output)
            output_dir.mkdir()

            # Create grid
            grid = _make_grid_json(params_dir / "grid.json", output_dir / "grid.json")
            _write_content(output_dir / "grid.csv", grid.to_csv())

            # Create people
            people = _make_people_json(
                params_dir / "people.json", output_dir / "people.json"
            )
            _write_content(output_dir / "people.csv", people.to_csv())

            # Create specimens
            specimens = _make_specimens_json(
                params_dir / "specimens.json", output_dir / "specimens.json", grid
            )
            _write_content(output_dir / "specimens.csv", specimens.to_csv())

            # Create assays
            assays_output_path = output_dir / "assays.json"
            _make_assays_json(
                params_dir / "assays.json", assays_output_path, people, specimens
            )
            assays_to_csv(assays_output_path, output)

            # Create database
            make_database(
                output_dir / "assays.csv",
                output_dir / "people.csv",
                output_dir / "specimens.csv",
                output_dir / "snailz.db",
            )

        except Exception as e:
            utils.fail(f"Error in 'all' command: {str(e)}")

## `assays(baseline=None, delay=None, degrade=None, mutant=None, noise=None, oops=None, output=None, params=None, people=None, plate_size=None, seed=None, specimens=None)`

Generate assays for specimens.

Source code in `src/snailz/clui.py`

    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    @cli.command()
    @click.option(
        "--baseline",
        type=float,
        help="Baseline reading value for non-susceptible specimens (must be > 0)",
    )
    @click.option(
        "--delay",
        type=int,
        help="Maximum days between specimen collection and assay (must be > 0)",
    )
    @click.option(
        "--degrade",
        type=float,
        help="Rate at which sample responses decrease per day after first day (0-1)",
    )
    @click.option(
        "--mutant", type=float, help="Reading value for susceptible specimens (must be > 0)"
    )
    @click.option("--noise", type=float, help="Noise level for readings (must be > 0)")
    @click.option(
        "--oops",
        type=float,
        help="Factor to multiply response values by for one random person (0 means no adjustment, must be >= 0)",
    )
    @click.option("--output", type=click.Path(), help="Path to JSON output file")
    @click.option(
        "--params", type=click.Path(exists=True), help="Path to JSON parameter file"
    )
    @click.option("--people", type=click.Path(exists=True), help="Path to people JSON file")
    @click.option("--plate-size", type=int, help="Size of assay plate (must be > 0)")
    @click.option("--seed", type=int, help="Random seed")
    @click.option(
        "--specimens", type=click.Path(exists=True), help="Path to specimens JSON file"
    )
    def assays(
        baseline=None,
        delay=None,
        degrade=None,
        mutant=None,
        noise=None,
        oops=None,
        output=None,
        params=None,
        people=None,
        plate_size=None,
        seed=None,
        specimens=None,
    ):
        """Generate assays for specimens."""
        try:
            # Load previously-generated data.
            people = utils.load_data("people", people, AllPersons)
            specimens = utils.load_data("specimens", specimens, AllSpecimens)

            # Type casting for the type checker
            people = cast(AllPersons, people)
            specimens = cast(AllSpecimens, specimens)

            # Create
            supplied = (
                ("baseline", baseline),
                ("delay", delay),
                ("degrade", degrade),
                ("mutant", mutant),
                ("noise", noise),
                ("oops", oops),
                ("plate_size", plate_size),
                ("seed", seed),
            )
            _make_assays_json(params, output, people, specimens, supplied)

        except Exception as e:
            utils.fail(f"Error generating assays: {str(e)}")

## `convert(input, kind, output)`

Convert JSON data to CSV format.

Converts grid, specimens, or assays data from JSON to CSV format. If output is not specified, writes to standard output.

Source code in `src/snailz/clui.py`

    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    @cli.command()
    @click.option(
        "--input",
        type=click.Path(exists=True),
        required=True,
        help="Path to input JSON file",
    )
    @click.option(
        "--kind",
        type=click.Choice(["assays", "grid", "people", "specimens"]),
        required=True,
        help="Type of data to convert",
    )
    @click.option(
        "--output",
        type=click.Path(),
        help="Path to output CSV file (should be a directory for assays)",
    )
    def convert(input, kind, output):
        """Convert JSON data to CSV format.

        Converts grid, specimens, or assays data from JSON to CSV format.
        If output is not specified, writes to standard output.
        """
        try:
            # Load the input file based on kind
            if kind == "assays":
                assays_to_csv(input, output)
            elif kind == "grid":
                data = utils.load_data("grid", input, Grid)
                grid_data = cast(Grid, data)
                content = grid_data.to_csv()
                _write_content(output, content)
            elif kind == "people":
                data = utils.load_data("people", input, AllPersons)
                people_data = cast(AllPersons, data)
                content = people_data.to_csv()
                _write_content(output, content)
            elif kind == "specimens":
                data = utils.load_data("specimens", input, AllSpecimens)
                specimens_data = cast(AllSpecimens, data)
                content = specimens_data.to_csv()
                _write_content(output, content)
            else:
                raise ValueError(f"unknown kind {kind}")
        except Exception as e:
            utils.fail(f"Error converting data: {str(e)}")

## `database(assays, output, people, specimens)`

Create a SQLite database from CSV files.

Source code in `src/snailz/clui.py`

    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    @cli.command()
    @click.option(
        "--assays",
        type=click.Path(exists=True),
        required=True,
        help="Path to assay CSV file",
    )
    @click.option(
        "--output",
        type=click.Path(),
        required=True,
        help="Path to SQLite database file to create",
    )
    @click.option(
        "--people",
        type=click.Path(exists=True),
        required=True,
        help="Path to people CSV file",
    )
    @click.option(
        "--specimens",
        type=click.Path(exists=True),
        required=True,
        help="Path to specimen CSV file",
    )
    def database(assays, output, people, specimens):
        """Create a SQLite database from CSV files."""
        try:
            make_database(assays, people, specimens, output)
        except Exception as e:
            utils.fail(f"Error creating database: {str(e)}")

## `grid(depth=None, output=None, params=None, seed=None, size=None)`

Generate grid.

Source code in `src/snailz/clui.py`

    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    @cli.command()
    @click.option("--depth", type=int, help="Grid depth")
    @click.option("--output", type=click.Path(), help="Path to JSON output file")
    @click.option(
        "--params", type=click.Path(exists=True), help="Path to JSON parameter file"
    )
    @click.option("--seed", type=int, help="Random seed")
    @click.option("--size", type=int, help="Grid size")
    def grid(
        depth=None,
        output=None,
        params=None,
        seed=None,
        size=None,
    ):
        """Generate grid."""
        try:
            supplied = (
                ("depth", depth),
                ("seed", seed),
                ("size", size),
            )
            _make_grid_json(params, output, supplied)
        except Exception as e:
            utils.fail(f"Error generating grid: {str(e)}")

## `init(output=None, overwrite=False)`

Initialize parameter files for snailz.

Creates JSON parameter files in the specified directory (or current directory if not specified). Creates the directory if it doesn't exist.

By default, will not overwrite existing files unless --overwrite is specified.

Source code in `src/snailz/clui.py`

    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    @cli.command()
    @click.option(
        "--output",
        type=click.Path(file_okay=False),
        help="Directory to create parameter files in (defaults to current directory)",
    )
    @click.option(
        "--overwrite",
        is_flag=True,
        default=False,
        help="Overwrite existing parameter files",
    )
    def init(output=None, overwrite=False):
        """Initialize parameter files for snailz.

        Creates JSON parameter files in the specified directory (or current directory
        if not specified). Creates the directory if it doesn't exist.

        By default, will not overwrite existing files unless --overwrite is specified.
        """
        try:
            output_dir = Path.cwd() if output is None else Path(output)
            output_dir.mkdir(parents=True, exist_ok=True)
            params = {
                "assays.json": defaults.DEFAULT_ASSAY_PARAMS,
                "grid.json": defaults.DEFAULT_GRID_PARAMS,
                "people.json": defaults.DEFAULT_PEOPLE_PARAMS,
                "specimens.json": defaults.DEFAULT_SPECIMEN_PARAMS,
            }

            if not overwrite:
                _check_not_overwriting(output_dir, params)

            for filename, param_obj in params.items():
                file_path = output_dir / filename
                with file_path.open("w") as writer:
                    param_dict = param_obj.model_dump()
                    for key, value in param_dict.items():
                        if isinstance(value, date):
                            param_dict[key] = value.isoformat()
                    json.dump(param_dict, writer, indent=4)
                    writer.write("\n")

        except Exception as e:
            utils.fail(f"Error creating parameter files: {str(e)}")

## `mangle(seed, dir, people)`

Modify assay files by reassigning people.

This command takes assay files in a directory and reassigns the people who performed the assays using the provided seed for random number generation.

Source code in `src/snailz/clui.py`

    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    @cli.command()
    @click.option(
        "--dir",
        type=click.Path(exists=True, file_okay=False, dir_okay=True),
        required=True,
        help="Directory containing assay CSV files",
    )
    @click.option(
        "--people",
        type=click.Path(exists=True),
        required=True,
        help="Path to people.csv file",
    )
    @click.option(
        "--seed", type=int, required=True, help="Random seed for assigning people"
    )
    def mangle(seed, dir, people):
        """Modify assay files by reassigning people.

        This command takes assay files in a directory and reassigns the people
        who performed the assays using the provided seed for random number generation.
        """
        try:
            random.seed(seed)
            mangle_assays(dir, people)
        except Exception as e:
            utils.fail(f"Error mangling assays: {str(e)}")

## `people(locale=None, number=None, output=None, params=None, seed=None)`

Generate people.

Source code in `src/snailz/clui.py`

    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    @cli.command()
    @click.option("--locale", type=str, help="Locale for generating people")
    @click.option("--number", type=int, help="Number of people to generate")
    @click.option("--output", type=click.Path(), help="Path to JSON output file")
    @click.option(
        "--params", type=click.Path(exists=True), help="Path to JSON parameter file"
    )
    @click.option("--seed", type=int, help="Random seed")
    def people(
        locale=None,
        number=None,
        output=None,
        params=None,
        seed=None,
    ):
        """Generate people."""
        try:
            supplied = (
                ("locale", locale),
                ("number", number),
                ("seed", seed),
            )
            _make_people_json(params, output, supplied)

        except Exception as e:
            utils.fail(f"Error generating people: {str(e)}")

## `specimens(grid=None, length=None, max_mass=None, min_mass=None, mut_scale=None, mutations=None, number=None, output=None, params=None, seed=None, start_date=None, end_date=None)`

Generate specimens.

Source code in `src/snailz/clui.py`

    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    @cli.command()
    @click.option("--grid", type=str, help="Path to grid JSON file")
    @click.option("--length", type=int, help="Length of each genome")
    @click.option("--max-mass", type=float, help="Maximum specimen mass")
    @click.option("--min-mass", type=float, help="Minimum specimen mass")
    @click.option("--mut-scale", type=float, help="Mutation scaling factor")
    @click.option("--mutations", type=int, help="Number of possible mutation loci")
    @click.option("--number", type=int, help="Number of specimens to generate")
    @click.option("--output", type=click.Path(), help="Path to JSON output file")
    @click.option(
        "--params", type=click.Path(exists=True), help="Path to JSON parameter file"
    )
    @click.option("--seed", type=int, help="Random seed")
    @click.option(
        "--start-date",
        callback=utils.validate_date,
        help="Start date for specimen collection (YYYY-MM-DD)",
    )
    @click.option(
        "--end-date",
        callback=utils.validate_date,
        help="End date for specimen collection (YYYY-MM-DD)",
    )
    def specimens(
        grid=None,
        length=None,
        max_mass=None,
        min_mass=None,
        mut_scale=None,
        mutations=None,
        number=None,
        output=None,
        params=None,
        seed=None,
        start_date=None,
        end_date=None,
    ):
        """Generate specimens."""
        try:
            # Load previously-generated data.
            grid_data = utils.load_data("grid", grid, Grid)

            # Type casting for the type checker
            grid = cast(Grid, grid_data)

            # Get parameters for specimen generation.
            supplied = (
                ("length", length),
                ("max_mass", max_mass),
                ("min_mass", min_mass),
                ("mut_scale", mut_scale),
                ("mutations", mutations),
                ("number", number),
                ("seed", seed),
                ("start_date", start_date),
                ("end_date", end_date),
            )
            _make_specimens_json(params, output, grid, supplied)

        except Exception as e:
            utils.fail(f"Error generating specimens: {str(e)}")

## `_get_params(caller, param_class, supplied, params_file, **converters)`

Get and check parameter values.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`caller` | `str` | Name of the calling function for error messages | required
`param_class` | `Type[BaseModelType]` | Pydantic parameter class | required
`supplied` | `tuple[tuple[str, object], ...]` | Tuple of (name, value) pairs for CLI parameters | required
`params_file` | `str | Path | None` | Path to JSON parameter file, or None | required
`converters` | `Callable` | Optional converters for specific parameter values | `{}`

Returns:

Type | Description
--- | ---
`BaseModelType` | An instance of the param_class with validated parameters

Source code in `src/snailz/clui.py`

    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    def _get_params(
        caller: str,
        param_class: Type[BaseModelType],
        supplied: tuple[tuple[str, object], ...],
        params_file: str | Path | None,
        **converters: Callable,
    ) -> BaseModelType:
        """Get and check parameter values.

        Parameters:
            caller: Name of the calling function for error messages
            param_class: Pydantic parameter class
            supplied: Tuple of (name, value) pairs for CLI parameters
            params_file: Path to JSON parameter file, or None
            converters: Optional converters for specific parameter values

        Returns:
            An instance of the param_class with validated parameters
        """
        # Read parameter file if given.
        result = {}
        if params_file:
            with open(params_file, "r") as f:
                result = json.load(f)
                for name, conv in converters.items():
                    if name in result:
                        result[name] = conv(result[name])

        # Override with extra parameters
        for name, value in supplied:
            if value is not None:
                result[name] = value

        # Create and return parameter object (does validation)
        return param_class(**result)

## `_write_content(output, content)`

Write content to standard output or a file.

Source code in `src/snailz/clui.py`

    573
    574
    575
    576
    577
    578
    579
    def _write_content(output: str | Path | None, content: str) -> None:
        """Write content to standard output or a file."""
        if output:
            with open(output, "w") as writer:
                writer.write(content)
        else:
            print(content, end="")