feat: scaffolding + init and print configuration
Some checks failed
/ Static Analysis (push) Failing after 29s
/ Tests (push) Failing after 16s

This commit is contained in:
Marc 2024-04-12 01:09:56 -04:00
commit dc57ecd228
Signed by: marc
GPG key ID: 048E042F22B5DC79
14 changed files with 671 additions and 0 deletions

View file

@ -0,0 +1,31 @@
on: [push]
jobs:
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v2
with:
python-version: 3.12
- run: |
pip install -r ./requirements.txt
pip install -r ./requirements_sast.txt
- name: Formatting
run: python -m black .
- name: Linting
run: python -m pylint .
test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v2
with:
python-version: 3.12
- run: |
pip install -r ./requirements.txt
pip install -r ./requirements_test.txt
- name: Test suites
run: python -m pytest

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*egg-info*
*venv*
*.pyc

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

27
pyproject.toml Normal file
View file

@ -0,0 +1,27 @@
[project]
name = "spud"
version = "0.0.0"
requires-python = "~=3.12"
dependencies = [
"click~=8.0",
"httpx",
"pydantic~=2.0",
"fastapi~=0.110.0",
"uvicorn[standard]~=0.29",
]
[project.optional-dependencies]
sast = [
"black",
"pylint"
]
test = [
"pytest"
]
[project.scripts]
spud = "spud.cli:cli"
[tool.setuptools]
packages = ["spud"]

60
requirements.txt Normal file
View file

@ -0,0 +1,60 @@
annotated-types==0.6.0
# via pydantic
anyio==4.3.0
# via
# httpx
# starlette
# watchfiles
certifi==2024.2.2
# via
# httpcore
# httpx
click==8.1.7
# via
# spud (pyproject.toml)
# uvicorn
fastapi==0.110.1
# via spud (pyproject.toml)
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.5
# via httpx
httptools==0.6.1
# via uvicorn
httpx==0.27.0
# via spud (pyproject.toml)
idna==3.7
# via
# anyio
# httpx
pydantic==2.6.4
# via
# fastapi
# spud (pyproject.toml)
pydantic-core==2.16.3
# via pydantic
python-dotenv==1.0.1
# via uvicorn
pyyaml==6.0.1
# via uvicorn
sniffio==1.3.1
# via
# anyio
# httpx
starlette==0.37.2
# via fastapi
typing-extensions==4.11.0
# via
# fastapi
# pydantic
# pydantic-core
uvicorn[standard]==0.29.0
# via spud (pyproject.toml)
uvloop==0.19.0
# via uvicorn
watchfiles==0.21.0
# via uvicorn
websockets==12.0
# via uvicorn

127
requirements_dev.txt Normal file
View file

@ -0,0 +1,127 @@
annotated-types==0.6.0
# via
# -c requirements.txt
# pydantic
anyio==4.3.0
# via
# -c requirements.txt
# httpx
# starlette
# watchfiles
astroid==3.1.0
# via pylint
black==24.3.0
# via spud (pyproject.toml)
certifi==2024.2.2
# via
# -c requirements.txt
# httpcore
# httpx
click==8.1.7
# via
# -c requirements.txt
# black
# spud (pyproject.toml)
# uvicorn
dill==0.3.8
# via pylint
fastapi==0.110.1
# via
# -c requirements.txt
# spud (pyproject.toml)
h11==0.14.0
# via
# -c requirements.txt
# httpcore
# uvicorn
httpcore==1.0.5
# via
# -c requirements.txt
# httpx
httptools==0.6.1
# via
# -c requirements.txt
# uvicorn
httpx==0.27.0
# via
# -c requirements.txt
# spud (pyproject.toml)
idna==3.7
# via
# -c requirements.txt
# anyio
# httpx
iniconfig==2.0.0
# via pytest
isort==5.13.2
# via pylint
mccabe==0.7.0
# via pylint
mypy-extensions==1.0.0
# via black
packaging==24.0
# via
# black
# pytest
pathspec==0.12.1
# via black
platformdirs==4.2.0
# via
# black
# pylint
pluggy==1.4.0
# via pytest
pydantic==2.6.4
# via
# -c requirements.txt
# fastapi
# spud (pyproject.toml)
pydantic-core==2.16.3
# via
# -c requirements.txt
# pydantic
pylint==3.1.0
# via spud (pyproject.toml)
pytest==8.1.1
# via spud (pyproject.toml)
python-dotenv==1.0.1
# via
# -c requirements.txt
# uvicorn
pyyaml==6.0.1
# via
# -c requirements.txt
# uvicorn
sniffio==1.3.1
# via
# -c requirements.txt
# anyio
# httpx
starlette==0.37.2
# via
# -c requirements.txt
# fastapi
tomlkit==0.12.4
# via pylint
typing-extensions==4.11.0
# via
# -c requirements.txt
# fastapi
# pydantic
# pydantic-core
uvicorn[standard]==0.29.0
# via
# -c requirements.txt
# spud (pyproject.toml)
uvloop==0.19.0
# via
# -c requirements.txt
# uvicorn
watchfiles==0.21.0
# via
# -c requirements.txt
# uvicorn
websockets==12.0
# via
# -c requirements.txt
# uvicorn

119
requirements_sast.txt Normal file
View file

@ -0,0 +1,119 @@
annotated-types==0.6.0
# via
# -c requirements.txt
# pydantic
anyio==4.3.0
# via
# -c requirements.txt
# httpx
# starlette
# watchfiles
astroid==3.1.0
# via pylint
black==24.3.0
# via spud (pyproject.toml)
certifi==2024.2.2
# via
# -c requirements.txt
# httpcore
# httpx
click==8.1.7
# via
# -c requirements.txt
# black
# spud (pyproject.toml)
# uvicorn
dill==0.3.8
# via pylint
fastapi==0.110.1
# via
# -c requirements.txt
# spud (pyproject.toml)
h11==0.14.0
# via
# -c requirements.txt
# httpcore
# uvicorn
httpcore==1.0.5
# via
# -c requirements.txt
# httpx
httptools==0.6.1
# via
# -c requirements.txt
# uvicorn
httpx==0.27.0
# via
# -c requirements.txt
# spud (pyproject.toml)
idna==3.7
# via
# -c requirements.txt
# anyio
# httpx
isort==5.13.2
# via pylint
mccabe==0.7.0
# via pylint
mypy-extensions==1.0.0
# via black
packaging==24.0
# via black
pathspec==0.12.1
# via black
platformdirs==4.2.0
# via
# black
# pylint
pydantic==2.6.4
# via
# -c requirements.txt
# fastapi
# spud (pyproject.toml)
pydantic-core==2.16.3
# via
# -c requirements.txt
# pydantic
pylint==3.1.0
# via spud (pyproject.toml)
python-dotenv==1.0.1
# via
# -c requirements.txt
# uvicorn
pyyaml==6.0.1
# via
# -c requirements.txt
# uvicorn
sniffio==1.3.1
# via
# -c requirements.txt
# anyio
# httpx
starlette==0.37.2
# via
# -c requirements.txt
# fastapi
tomlkit==0.12.4
# via pylint
typing-extensions==4.11.0
# via
# -c requirements.txt
# fastapi
# pydantic
# pydantic-core
uvicorn[standard]==0.29.0
# via
# -c requirements.txt
# spud (pyproject.toml)
uvloop==0.19.0
# via
# -c requirements.txt
# uvicorn
watchfiles==0.21.0
# via
# -c requirements.txt
# uvicorn
websockets==12.0
# via
# -c requirements.txt
# uvicorn

119
requirements_test.txt Normal file
View file

@ -0,0 +1,119 @@
annotated-types==0.6.0
# via
# -c requirements.txt
# pydantic
anyio==4.3.0
# via
# -c requirements.txt
# httpx
# starlette
# watchfiles
astroid==3.1.0
# via pylint
black==24.3.0
# via spud (pyproject.toml)
certifi==2024.2.2
# via
# -c requirements.txt
# httpcore
# httpx
click==8.1.7
# via
# -c requirements.txt
# black
# spud (pyproject.toml)
# uvicorn
dill==0.3.8
# via pylint
fastapi==0.110.1
# via
# -c requirements.txt
# spud (pyproject.toml)
h11==0.14.0
# via
# -c requirements.txt
# httpcore
# uvicorn
httpcore==1.0.5
# via
# -c requirements.txt
# httpx
httptools==0.6.1
# via
# -c requirements.txt
# uvicorn
httpx==0.27.0
# via
# -c requirements.txt
# spud (pyproject.toml)
idna==3.7
# via
# -c requirements.txt
# anyio
# httpx
isort==5.13.2
# via pylint
mccabe==0.7.0
# via pylint
mypy-extensions==1.0.0
# via black
packaging==24.0
# via black
pathspec==0.12.1
# via black
platformdirs==4.2.0
# via
# black
# pylint
pydantic==2.6.4
# via
# -c requirements.txt
# fastapi
# spud (pyproject.toml)
pydantic-core==2.16.3
# via
# -c requirements.txt
# pydantic
pylint==3.1.0
# via spud (pyproject.toml)
python-dotenv==1.0.1
# via
# -c requirements.txt
# uvicorn
pyyaml==6.0.1
# via
# -c requirements.txt
# uvicorn
sniffio==1.3.1
# via
# -c requirements.txt
# anyio
# httpx
starlette==0.37.2
# via
# -c requirements.txt
# fastapi
tomlkit==0.12.4
# via pylint
typing-extensions==4.11.0
# via
# -c requirements.txt
# fastapi
# pydantic
# pydantic-core
uvicorn[standard]==0.29.0
# via
# -c requirements.txt
# spud (pyproject.toml)
uvloop==0.19.0
# via
# -c requirements.txt
# uvicorn
watchfiles==0.21.0
# via
# -c requirements.txt
# uvicorn
websockets==12.0
# via
# -c requirements.txt
# uvicorn

12
script/bootstrap.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/bash
VENV=spud.venv
python -m venv --upgrade "$VENV"
. "$VENV/bin/activate"
pip install -U pip pip-tools
pip-sync requirements.txt requirements_dev.txt
pip install -e .

33
script/lock-deps.sh Executable file
View file

@ -0,0 +1,33 @@
#!/bin/bash
echo "Generating requirements.txt..."
python -m piptools compile \
-o requirements.txt \
pyproject.toml \
--no-header
echo "Generating requirements_dev.txt..."
python -m piptools compile \
-o requirements_dev.txt \
--no-header \
--extra sast \
--extra test \
--constraint requirements.txt \
pyproject.toml
python -m piptools compile \
-o requirements_sast.txt \
--no-header \
--extra sast \
--constraint requirements.txt \
pyproject.toml
python -m piptools compile \
-o requirements_test.txt \
--no-header \
--extra sast \
--constraint requirements.txt \
pyproject.toml

9
spud/base.py Normal file
View file

@ -0,0 +1,9 @@
"""
Models and datastructures
"""
import pydantic
class Configuration(pydantic.BaseModel):
"""Command line application configuration options"""

71
spud/cli.py Normal file
View file

@ -0,0 +1,71 @@
"""
Command-line application entry-point.
Logic for the command-line spud tooling.
"""
import pathlib
import json
import click
from spud.base import Configuration
DEFAULT_CONFIGURATION_PATH = "~/.config/spud/config.json"
@click.group()
@click.option(
"--config",
type=str,
default=None,
help="Configuration path",
)
@click.pass_context
def cli(context, config):
"""CLI root"""
context.ensure_object(dict)
config = config if config is not None else DEFAULT_CONFIGURATION_PATH
context.obj["config_path"] = pathlib.Path(config).expanduser()
@click.command()
@click.pass_context
def init(context):
"""Generates a default configuration and writes it to disk."""
config_path = context.obj["config_path"]
config_path.parent.mkdir(parents=True, exist_ok=True)
if config_path.exists():
raise RuntimeError(
f"File already exists ({str(config_path)}), cannot initialize."
)
default_configuration = Configuration()
with open(config_path, "w", encoding="utf8") as config_file:
config_file.write(default_configuration.model_dump_json(indent=2))
@click.command()
@click.pass_context
def print_config(context):
"""Prints the current configuration."""
config_path = context.obj["config_path"]
if not config_path.exists():
raise RuntimeError(f"Configuration file not found at {str(config_path)}.")
with open(config_path, "r", encoding="utf8") as config_file:
config_json = json.loads(config_file.read())
config = Configuration(**config_json)
click.echo(config.model_dump_json(indent=2))
cli.add_command(init)
cli.add_command(print_config)
if __name__ == "__main__":
cli(None, None)

18
tests/conftest.py Normal file
View file

@ -0,0 +1,18 @@
import pytest
import click.testing
import importlib
import spud.cli
@pytest.fixture(autouse=True)
def mock_default_config_path(monkeypatch, tmpdir):
monkeypatch.setattr(spud.cli, "DEFAULT_CONFIGURATION_PATH", tmpdir / ".config" / "spud" / "config.json")
@pytest.fixture(name="invoke_cli")
def invoke_cli_fixture():
def _call_cli(args: list[str]):
runner = click.testing.CliRunner()
return runner.invoke(spud.cli.cli, args)
return _call_cli

40
tests/test_cli.py Normal file
View file

@ -0,0 +1,40 @@
import pytest
import click
import pathlib
def test_init_raises_if_config_file_exists_default_path(invoke_cli, tmpdir, monkeypatch):
expected_default_path = tmpdir / ".config" / "spud" / "config.json"
pathlib.Path(expected_default_path).parent.mkdir(parents=True)
expected_default_path.write("{}")
result = invoke_cli(["init"])
assert result.exit_code == 1
assert str(result.exception) == f"File already exists ({str(expected_default_path)}), cannot initialize."
def test_init_raises_if_config_file_exists_custom_path(invoke_cli, tmpdir, monkeypatch):
expected_default_path = tmpdir / "config.json"
expected_default_path.write("{}")
result = invoke_cli(["--config", str(expected_default_path), "init"])
assert result.exit_code == 1
assert str(result.exception) == f"File already exists ({str(expected_default_path)}), cannot initialize."
def test_print_config_raises_if_no_config_file_default_path(invoke_cli, tmpdir, monkeypatch):
expected_default_path = tmpdir / ".config" / "spud" / "config.json"
result = invoke_cli(["print-config"])
assert result.exit_code == 1
assert str(result.exception) == f"Configuration file not found at {str(expected_default_path)}."
def test_print_config_raises_if_no_config_file_custom_path(invoke_cli, tmpdir, monkeypatch):
config_file = tmpdir / "config.json"
result = invoke_cli(["--config", str(config_file), "print-config"])
assert result.exit_code == 1
assert str(result.exception) == f"Configuration file not found at {str(config_file)}."