Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.14
2 changes: 2 additions & 0 deletions data/inputs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
2 changes: 2 additions & 0 deletions data/intermediates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
2 changes: 2 additions & 0 deletions data/outputs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
107 changes: 103 additions & 4 deletions notebooks/checks.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ from enum import Enum
from pathlib import Path

class Severity(Enum):
"""
Severity levels for manifest validation results.

Attributes
----------
PASS : str
The check succeeded.
WARNING : str
The check surfaced a non-blocking issue.
ERROR : str
The check found a blocking issue.
"""
PASS = "pass"
WARNING = "warning"
ERROR = "error"
Expand All @@ -81,7 +93,18 @@ PRINCIPLES = {
@dataclass
class CheckResult:
"""
Result from a single Stagecoach manifest check.
Represent the outcome of a single manifest check.

Attributes
----------
name : str
Stable identifier for the check.
state : Severity
Severity assigned to the check result.
message : str
Human-readable explanation of the outcome.
principle : int | list[int]
Frontier principle number, or numbers, associated with the check.
"""
name: str
state: Severity
Expand Down Expand Up @@ -111,6 +134,19 @@ Now, let's define a few checks:

```{python}
def check_project_exists(directory: Path) -> CheckResult:
"""
Verify that the project directory exists.

Parameters
----------
directory : Path
Directory declared as the project working directory.

Returns
-------
CheckResult
Passes when ``directory`` exists and is a directory.
"""
if directory.exists() and directory.is_dir():
return CheckResult(
name="project_exists",
Expand All @@ -127,9 +163,17 @@ def check_project_exists(directory: Path) -> CheckResult:

def check_git_repo_exists(directory: Path) -> CheckResult:
"""
Check that the project directory is a git repository.
Verify that the project directory is under Git version control.

Pass/Fail, no exceptions.
Parameters
----------
directory : Path
Project directory to inspect.

Returns
-------
CheckResult
Passes when a ``.git`` directory is present.
"""
if (directory / ".git").exists():
return CheckResult(
Expand All @@ -147,6 +191,20 @@ def check_git_repo_exists(directory: Path) -> CheckResult:
)

def check_environment_exists(directory: Path) -> CheckResult:
"""
Look for a project environment specification or lockfile.

Parameters
----------
directory : Path
Project directory to inspect.

Returns
-------
CheckResult
Passes when at least one supported environment file exists and
warns otherwise.
"""
candidates = [
"rv.lock",
"renv.lock",
Expand Down Expand Up @@ -187,6 +245,20 @@ started writing code:
#| sorting-hat: keep

def check_code_exists(directory: Path) -> CheckResult:
"""
Check whether the project contains analysis or source code files.

Parameters
----------
directory : Path
Project directory to inspect recursively.

Returns
-------
CheckResult
Passes when at least one supported code or notebook file is found
outside ignored directories.
"""
patterns = [
"**/*.py",
"**/*.R",
Expand Down Expand Up @@ -241,6 +313,20 @@ of things like READMEs, notebooks, or markdown files:

```{python}
def check_narrative_exists(directory: Path) -> CheckResult:
"""
Check whether the project contains narrated analysis notebooks.

Parameters
----------
directory : Path
Project directory to inspect recursively.

Returns
-------
CheckResult
Passes when Quarto, R Markdown, or Jupyter notebooks are present
outside ignored directories.
"""

patterns = [
"**/*.qmd",
Expand Down Expand Up @@ -286,6 +372,20 @@ def check_narrative_exists(directory: Path) -> CheckResult:
)

def check_readme_exists(directory: Path) -> CheckResult:
"""
Check whether the project contains a top-level README document.

Parameters
----------
directory : Path
Project directory to inspect.

Returns
-------
CheckResult
Passes when a supported README filename exists and errors
otherwise.
"""
if (directory / "README.md").exists() or (directory / "README.Rmd").exists() or (directory / "README.qmd").exists() or (directory / "README").exists():
return CheckResult(
name="readme_exists",
Expand Down Expand Up @@ -337,4 +437,3 @@ def check_project_structure(directory: Path) -> CheckResult:

In the next module, we define the ManifestChecker class, which will run all of these
checks and more, and return a structured report that can be displayed in `stagecoach inspect`.

68 changes: 63 additions & 5 deletions notebooks/cli.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@ Once the CLI is defined as "hailing" the stagecoach, stagecoach will
run `issue_manifest` to create the manifest,
and then proceed with the rest of the stagecoach.

Here's the code for the CLI. First, hailing the stagecoach:

```{python}
#| sorting-hat: keep

from pathlib import Path
from enum import Enum
from rich.console import Console
from rich.panel import Panel
from typing import Annotated
from sheriff.sheriff import Sheriff
from stagecoach.stagecoach import StageCoach
from stagecoach.checks import Severity, PRINCIPLES
from stagecoach.checks import Severity
from stagecoach.ui import failure_panel
import typer

Expand All @@ -50,6 +51,16 @@ app = typer.Typer(
)

class FailureLevel(str, Enum):
"""
Severity thresholds exposed by the CLI.

Attributes
----------
WARNING : str
Treat warnings as command failures.
ERROR : str
Treat only errors as command failures.
"""
WARNING = "warning"
ERROR = "error"

Expand All @@ -66,6 +77,11 @@ def hail(
"-o",
help="Where to write the manifest.",
),
outpost: bool = typer.Option(
False,
"--outpost/--no-outpost",
help="Whether to create a temporary mock Frontier citizenship scaffold for manifest creation outside the assigned Frontier.",
),
overwrite: bool = typer.Option(
False,
"--overwrite/--no-overwrite",
Expand All @@ -74,6 +90,15 @@ def hail(
) -> None:
"""
Create a Stagecoach manifest.

Parameters
----------
interactive : bool, default=True
Whether to prompt for manifest fields interactively.
output_path : Path, default=Path("stagecoach_manifest.yml")
Destination path for the generated manifest.
overwrite : bool, default=False
Whether to overwrite an existing manifest file.
"""

console = Console()
Expand All @@ -87,13 +112,16 @@ def hail(
).hail(
interactive=interactive,
overwrite=overwrite,
outpost=outpost
)

except Exception as exc:
failure_panel(console, str(exc))
raise typer.Exit(code=1)
```

Inspecting the manifest:

```{python}
@app.command()
def inspect(
Expand All @@ -116,6 +144,13 @@ def inspect(
) -> None:
"""
Inspect a Stagecoach manifest.

Parameters
----------
manifest_path : Path, default=Path("stagecoach_manifest.yml")
Path to the manifest to validate.
level : FailureLevel, default=FailureLevel.ERROR
Minimum severity that should cause the command to exit with failure.
"""

console = Console()
Expand All @@ -137,17 +172,40 @@ def inspect(
raise typer.Exit(code=1)
```

And, staging:

```{python}
@app.command()
def stage():
def stage(
manifest_path: Path = typer.Option(
Path("stagecoach_manifest.yml"),
"--manifest",
"-m",
help="Path to the manifest to inspect.",
)
):
"""
Stage data declared by the manifest.

Returns
-------
None
The command exits with a nonzero status when staging fails.
"""
console = Console()
customs_sheriff = Sheriff(console)
pass
staged = StageCoach(
sheriff=customs_sheriff,
console=console,
manifest_path=manifest_path
).stage()
if not staged:
raise typer.Exit(code=1)

```

```{python}
#| eval: false
if __name__ == "__main__":
app()
```

Loading
Loading