Skip to content

Commit

Permalink
Added tooling for working with the alias file
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisgKent committed Jul 2, 2024
1 parent e050ab2 commit 5560ac2
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 12 deletions.
73 changes: 65 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,71 @@ $ primal-page [OPTIONS] COMMAND [ARGS]...

**Commands**:

* `aliases`: Manage aliases
* `build-index`: Build an index.json file from all schemes...
* `create`: Create a new scheme in the required format
* `dev`: Development commands
* `download`: Download schemes from the index.json
* `modify`: Modify an existing scheme's metadata...
* `remove`: Remove a scheme's version from the repo,...

## `primal-page aliases`

Manage aliases

**Usage**:

```console
$ primal-page aliases [OPTIONS] COMMAND [ARGS]...
```

**Options**:

* `--help`: Show this message and exit.

**Commands**:

* `add`: Add an alias:schemeid to the alias file
* `remove`: Remove an alias from the alias file

### `primal-page aliases add`

Add an alias:schemeid to the alias file

**Usage**:

```console
$ primal-page aliases add [OPTIONS] ALIASES_FILE ALIAS SCHEMEID
```

**Arguments**:

* `ALIASES_FILE`: The path to the alias file to write to [required]
* `ALIAS`: The alias to add [required]
* `SCHEMEID`: The schemeid to add the alias refers to. In the form of 'schemename/ampliconsize/schemeversion' [required]

**Options**:

* `--help`: Show this message and exit.

### `primal-page aliases remove`

Remove an alias from the alias file

**Usage**:

```console
$ primal-page aliases remove [OPTIONS] ALIASES_FILE ALIAS
```

**Arguments**:

* `ALIASES_FILE`: The path to the alias file to write to [required]
* `ALIAS`: The alias to add [required]

**Options**:

* `--help`: Show this message and exit.

## `primal-page build-index`

Build an index.json file from all schemes in the directory
Expand Down Expand Up @@ -284,7 +342,6 @@ $ primal-page create [OPTIONS] SCHEMEPATH
* `--fix / --no-fix`: Attempt to fix the scheme [default: no-fix]
* `--help`: Show this message and exit.


## `primal-page download`

Download schemes from the index.json
Expand All @@ -301,17 +358,17 @@ $ primal-page download [OPTIONS] COMMAND [ARGS]...

**Commands**:

* `download-all`: Download all schemes from the index.json
* `download-scheme`: Download a scheme from the index.json
* `all`: Download all schemes from the index.json
* `scheme`: Download a scheme from the index.json

### `primal-page download download-all`
### `primal-page download all`

Download all schemes from the index.json

**Usage**:

```console
$ primal-page download download-all [OPTIONS]
$ primal-page download all [OPTIONS]
```

**Options**:
Expand All @@ -320,14 +377,14 @@ $ primal-page download download-all [OPTIONS]
* `--index-url TEXT`: The URL to the index.json [default: https://raw.githubusercontent.com/quick-lab/primerschemes/main/index.json]
* `--help`: Show this message and exit.

### `primal-page download download-scheme`
### `primal-page download scheme`

Download a scheme from the index.json

**Usage**:

```console
$ primal-page download download-scheme [OPTIONS] SCHEMENAME AMPLICONSIZE SCHEMEVERSION
$ primal-page download scheme [OPTIONS] SCHEMENAME AMPLICONSIZE SCHEMEVERSION
```

**Arguments**:
Expand Down
Binary file removed dist/primal_page-0.1.0-py3-none-any.whl
Binary file not shown.
Binary file removed dist/primal_page-0.1.0.tar.gz
Binary file not shown.
Binary file removed dist/primal_page-1.0.0-py3-none-any.whl
Binary file not shown.
Binary file removed dist/primal_page-1.0.0.tar.gz
Binary file not shown.
113 changes: 113 additions & 0 deletions primal_page/aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import json
import pathlib
import re

import typer
from typing_extensions import Annotated

from primal_page.schemas import validate_scheme_id

app = typer.Typer(no_args_is_help=True)

ALIASES_PATTERN = r"^[a-z0-9][a-z0-9-.]*[a-z0-9]$"


def validate_alias(alias: str) -> str:
if not re.match(ALIASES_PATTERN, alias):
raise typer.BadParameter(
f"({alias}). Must only contain a-z, 0-9, and -. Cannot start or end with -"
)
return alias


@app.command(no_args_is_help=True)
def remove(
aliases_file: Annotated[
pathlib.Path,
typer.Argument(
help="The path to the alias file to write to",
exists=True,
file_okay=True,
writable=True,
),
],
alias: Annotated[
str,
typer.Argument(
help="The alias to add",
# No callback here because we don't want to validate removing the alias
),
],
):
"""
Remove an alias from the alias file
"""
# Read in the info.json file
with open(aliases_file) as f:
aliases = json.load(f)

# Remove the alias, if it exists
aliases.pop(alias, None)

# Write the new info.json file
with open(aliases_file, "w") as f:
json.dump(aliases, f, indent=4, sort_keys=True)


@app.command(no_args_is_help=True)
def add(
aliases_file: Annotated[
pathlib.Path,
typer.Argument(
help="The path to the alias file to write to",
exists=True,
file_okay=True,
writable=True,
),
],
alias: Annotated[
str,
typer.Argument(
help="The alias to add",
callback=validate_alias,
),
],
schemeid: Annotated[
str,
typer.Argument(
help="The schemeid to add the alias refers to. In the form of 'schemename/ampliconsize/schemeversion'"
),
],
):
"""
Add an alias:schemeid to the alias file
"""
# Parse the schemeid
schemename, ampliconsize, schemeversion = validate_scheme_id(schemeid)

# Read in the info.json file
with open(aliases_file) as f:
aliases = json.load(f)

# Check if the alias already exists
if alias in aliases:
raise typer.BadParameter(f"({alias}) already exists in the alias file")

# Add the alias
aliases[alias] = "/".join([schemename, ampliconsize, schemeversion])

# Write the new info.json file
with open(aliases_file, "w") as f:
json.dump(aliases, f, indent=4, sort_keys=True)


def parse_alias(aliases_file: pathlib.Path, alias: str) -> str:
with open(aliases_file) as f:
aliases = json.load(f)
if alias not in aliases:
raise typer.BadParameter(f"({alias}) does not exist in the alias file")
return aliases[alias]


if __name__ == "__main__":
app()
28 changes: 28 additions & 0 deletions primal_page/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,31 @@ class InvalidReference(UsageError):

def __init__(self, message: str):
super().__init__(message)


class InvalidSchemeID(UsageError):
"""Raised when a schemeid is invalid"""

def __init__(self, message: str):
super().__init__(message)


class InvalidSchemeName(UsageError):
"""Raised when a schemename is invalid"""

def __init__(self, message: str):
super().__init__(message)


class InvalidAmpliconSize(UsageError):
"""Raised when a schemeversion is invalid"""

def __init__(self, message: str):
super().__init__(message)


class InvalidSchemeVersion(UsageError):
"""Raised when a schemeversion is invalid"""

def __init__(self, message: str):
super().__init__(message)
2 changes: 2 additions & 0 deletions primal_page/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing_extensions import Annotated

from primal_page.__init__ import __version__
from primal_page.aliases import app as aliases_app
from primal_page.bedfiles import (
BEDFileResult,
BedfileVersion,
Expand Down Expand Up @@ -50,6 +51,7 @@ class FindResult(Enum):
help="Download schemes from the index.json",
)
app.add_typer(dev_app, name="dev", help="Development commands", hidden=True)
app.add_typer(aliases_app, name="aliases", help="Manage aliases")


def typer_callback_version(value: bool):
Expand Down
50 changes: 48 additions & 2 deletions primal_page/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from pydantic.functional_validators import AfterValidator

from primal_page.bedfiles import BedfileVersion
from primal_page.errors import (
InvalidSchemeID,
InvalidSchemeName,
InvalidSchemeVersion,
)

INFO_SCHEMA = "v2.0.0"

Expand All @@ -27,17 +32,58 @@ class SchemeStatus(Enum):
VALIDATED = "validated"


def validate_scheme_id(schemeid) -> tuple[str, str, str]:
"""
Parse the schemeid into its components
:raises InvalidSchemeID: if the schemeid is invalid
"""
try:
(
schemename,
ampliconsize,
schemeversion,
) = schemeid.split("/")
except ValueError as e:
raise InvalidSchemeID(
f"{schemeid} needs to be in the form (schemename)/(ampliconsize)/(schemeversion)"
) from e

# Validate each part separately
try:
schemename = validate_schemename(schemename)
schemeversion = validate_schemeversion(schemeversion)
ampliconsize = int(ampliconsize)
except InvalidSchemeName as e:
raise InvalidSchemeID(
f"{schemename} is an invalid schemename in {schemeid}"
) from e
except InvalidSchemeVersion as e:
raise InvalidSchemeID(
f"{schemeversion} is an invalid schemeversion in {schemeid}"
) from e
except ValueError as e:
raise InvalidSchemeID(
f"{ampliconsize} is an invalid ampliconsize in {schemeid}"
) from e

return schemename, str(ampliconsize), schemeversion


def validate_schemeversion(version: str) -> str:
if not re.match(VERSION_PATTERN, version):
raise ValueError(
raise InvalidSchemeVersion(
f"Invalid version: {version}. Must match be in form of v(int).(int).(int)"
)
return version


def validate_schemename(schemename: str) -> str:
"""
Validate the schemename
:raises InvalidSchemeName: if the schemename is invalid
"""
if not re.match(SCHEMENAME_PATTERN, schemename):
raise ValueError(
raise InvalidSchemeName(
f"Invalid schemename: {schemename}. Must only contain a-z, 0-9, and -. Cannot start or end with -"
)
return schemename
Expand Down
Loading

0 comments on commit 5560ac2

Please sign in to comment.