feat: scaffolding + init and print configuration
This commit is contained in:
commit
dc57ecd228
14 changed files with 671 additions and 0 deletions
31
.forgejo/workflows/main.yml
Normal file
31
.forgejo/workflows/main.yml
Normal 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
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*egg-info*
|
||||
*venv*
|
||||
*.pyc
|
||||
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
|||
3.12
|
27
pyproject.toml
Normal file
27
pyproject.toml
Normal 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
60
requirements.txt
Normal 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
127
requirements_dev.txt
Normal 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
119
requirements_sast.txt
Normal 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
119
requirements_test.txt
Normal 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
12
script/bootstrap.sh
Executable 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
33
script/lock-deps.sh
Executable 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
9
spud/base.py
Normal 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
71
spud/cli.py
Normal 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
18
tests/conftest.py
Normal 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
40
tests/test_cli.py
Normal 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)}."
|
Reference in a new issue