Skip to content

Commit 0f73efa

Browse files
committed
Initial CI build with MISE-EN-PLACE
0 parents  commit 0f73efa

File tree

7 files changed

+449
-0
lines changed

7 files changed

+449
-0
lines changed

.gitignore

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
download/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
26+
# Virtual environment
27+
.venv/
28+
venv/
29+
ENV/
30+
env/
31+
32+
# Pytest
33+
.pytest_cache/
34+
35+
# Coverage
36+
coverage.xml
37+
htmlcov/
38+
tests/coverage/
39+
.coverage
40+
41+
# Mypy
42+
.mypy_cache/
43+
44+
# pyenv
45+
.python-version
46+
47+
# pipenv
48+
Pipfile.lock
49+
50+
# Editor directories and files
51+
.vscode/
52+
.idea/
53+
*.sublime-project
54+
*.sublime-workspace
55+
56+
# macOS
57+
.DS_Store
58+
59+
# logs
60+
*.log
61+
62+
# GitHub Actions
63+
.github/workflows/
64+
65+
# local config
66+
.env
67+
68+
# misc
69+
*.bak
70+
*.swp
71+
72+
# MISE-EN-PLACE
73+
.mise_config
74+
.mise_trusted

mise.toml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
min_version = "2025.10.0"
2+
3+
[env]
4+
# Use the project name derived from the current directory
5+
PROJECT_NAME = "{{ config_root | basename }}"
6+
7+
# Make Python imports resolve to local project root directory's Virtual Environment
8+
PYTHONPATH = "{{ config_root }}"
9+
10+
# Automatic virtualenv activation
11+
_.python.venv = { path = ".venv", create = true, python = "3.13.7" }
12+
13+
[tools]
14+
python = "{{ get_env(name='PYTHON_VERSION', default='3.13.7') }}"
15+
16+
[tasks.install]
17+
description = "Install dependencies"
18+
run = "pip install -r requirements.txt"
19+
20+
[tasks.run]
21+
description = "Run the application"
22+
run = "python utilities/util.py"
23+
depends = ["test"]
24+
25+
[tasks.test]
26+
description = "Run tests"
27+
run = "pytest tests/"
28+
depends = ["lint", "install"]
29+
30+
[tasks.coverage]
31+
description = "Run tests with coverage and generate reports"
32+
# Use PYTHONPATH per env setting; outputs go to tests/coverage
33+
run = "pytest --cov=utilities --cov-report=xml:tests/coverage/coverage.xml --cov-report=html:tests/coverage/html tests/"
34+
depends = ["install", "lint"]
35+
36+
[tasks.lint]
37+
description = "Lint the code"
38+
run = "pylint utilities/"
39+
depends = ["install"]
40+
41+
[tasks.info]
42+
description = "Print project information"
43+
run = '''
44+
echo "Project: $PROJECT_NAME"
45+
echo "Virtual Environment: $VIRTUAL_ENV"
46+
'''
47+
depends = ["run"]
48+
49+
[tasks.build]
50+
description = "Build the code"
51+
run = "echo 'Build workflow complete'"
52+
depends = ["install", "lint", "coverage", "test", "run", "info"]

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
argcomplete==3.6.2
2+
click==8.2.1
3+
packaging==25.0
4+
pipx==1.7.1
5+
platformdirs==4.3.8
6+
userpath==1.9.2
7+
pytest>=7.0,<8.0
8+
pylint>=2.17,<3.0
9+
pytest-cov>=4.1.0

scripts/trust-mise.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# scripts/trust-mise.sh
3+
# Run `mise trust` once for the current directory's `mise.toml` and record the action
4+
5+
set -euo pipefail
6+
7+
# Make the script itself executable so future runs can be invoked directly
8+
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
9+
if [ ! -x "$SCRIPT_PATH" ]; then
10+
chmod +x "$SCRIPT_PATH" || true
11+
fi
12+
13+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14+
MARKER_FILE="$ROOT_DIR/.mise_trusted"
15+
16+
echo "Project root: $ROOT_DIR"
17+
18+
if [ -f "$MARKER_FILE" ]; then
19+
echo "mise.toml already trusted (marker present at $MARKER_FILE). Skipping."
20+
exit 0
21+
fi
22+
23+
if ! command -v mise >/dev/null 2>&1; then
24+
echo "mise is not installed or not on PATH. Install or add mise to PATH and re-run this script." >&2
25+
exit 2
26+
fi
27+
28+
echo "Running: mise trust"
29+
if mise trust; then
30+
# Prepare marker contents: ISO8601 UTC timestamp and IP
31+
TIMESTAMP_UTC=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
32+
33+
# Try to obtain public IP (short timeout). Fall back to local interface addresses.
34+
PUBLIC_IP=""
35+
if command -v curl >/dev/null 2>&1; then
36+
PUBLIC_IP=$(curl -s --max-time 3 https://api.ipify.org || true)
37+
if [ -z "$PUBLIC_IP" ]; then
38+
PUBLIC_IP=$(curl -s --max-time 3 https://ifconfig.co || true)
39+
fi
40+
fi
41+
42+
LOCAL_IP=""
43+
# macOS: try ipconfig, Linux: try hostname -I or ip
44+
if [ -z "$PUBLIC_IP" ]; then
45+
if command -v ip >/dev/null 2>&1; then
46+
LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -n1 || true)
47+
fi
48+
if [ -z "$LOCAL_IP" ] && command -v hostname >/dev/null 2>&1; then
49+
# hostname -I is not available on macOS; try ipconfig on mac
50+
if command -v ipconfig >/dev/null 2>&1; then
51+
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || true)
52+
fi
53+
if [ -z "$LOCAL_IP" ]; then
54+
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || true)
55+
fi
56+
fi
57+
fi
58+
59+
# Choose which IP to log
60+
if [ -n "$PUBLIC_IP" ]; then
61+
CHOSEN_IP="$PUBLIC_IP"
62+
elif [ -n "$LOCAL_IP" ]; then
63+
CHOSEN_IP="$LOCAL_IP"
64+
else
65+
CHOSEN_IP="unknown"
66+
fi
67+
68+
cat > "$MARKER_FILE" <<EOF
69+
trusted: true
70+
timestamp_utc: $TIMESTAMP_UTC
71+
ip: $CHOSEN_IP
72+
EOF
73+
74+
echo "mise.toml trusted and marker created at $MARKER_FILE"
75+
else
76+
echo "mise trust failed." >&2
77+
exit 3
78+
fi
79+
80+
exit 0

tests/test_util.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Unit tests for utilities.util module."""
2+
3+
import pytest
4+
from utilities.util import (
5+
TemperatureFormatNotSupported,
6+
_to_float_temperature,
7+
convert_farenheit_to_celsius,
8+
convert_celsius_to_farenheit,
9+
)
10+
11+
12+
@pytest.mark.parametrize(
13+
"temperature, expected",
14+
[
15+
(100, 100.0),
16+
(36.6, 36.6),
17+
(" 12.5 ", 12.5),
18+
],
19+
)
20+
def test_to_float_valid_inputs(temperature, expected):
21+
"""Test _to_float_temperature with valid inputs (parameterized).
22+
23+
Args:
24+
temperature: Input temperature value (int, float, or numeric string).
25+
expected: Expected float output temperature.
26+
"""
27+
assert _to_float_temperature(temperature) == expected
28+
29+
30+
def test_to_float_invalid_empty_string():
31+
"""Test _to_float_temperature with an empty string."""
32+
33+
with pytest.raises(TemperatureFormatNotSupported) as exc:
34+
_to_float_temperature("")
35+
assert "Empty string" in str(exc.value)
36+
37+
38+
def test_to_float_invalid_string():
39+
"""Test _to_float_temperature with a non-numeric string."""
40+
41+
with pytest.raises(TemperatureFormatNotSupported) as exc:
42+
_to_float_temperature("abc")
43+
assert "is not a valid temperature" in str(exc.value)
44+
45+
46+
@pytest.mark.parametrize(
47+
"temperature, expected",
48+
[
49+
(32, 0.0),
50+
(212, 100.0),
51+
("212", 100.0),
52+
],
53+
)
54+
def test_convert_fahrenheit_to_celsius(temperature, expected):
55+
"""Test convert_farenheit_to_celsius with valid inputs.
56+
57+
Args:
58+
temperature: Input temperature in Fahrenheit (int, float, or numeric string).
59+
expected: Expected float output temperature in Celsius.
60+
"""
61+
62+
assert convert_farenheit_to_celsius(temperature) == pytest.approx(expected)
63+
64+
65+
@pytest.mark.parametrize(
66+
"temperature, expected",
67+
[
68+
(0, 32.0),
69+
(100, 212.0),
70+
("100", 212.0),
71+
],
72+
)
73+
def test_convert_celsius_to_fahrenheit(temperature, expected):
74+
"""Test convert_celsius_to_farenheit with valid inputs.
75+
76+
Args:
77+
temperature: Input temperature in Celsius (int, float, or numeric string).
78+
expected: Expected float output temperature in Fahrenheit.
79+
"""
80+
81+
assert convert_celsius_to_farenheit(temperature) == pytest.approx(expected)
82+
83+
84+
@pytest.mark.parametrize(
85+
"temperature",
86+
[[], {}, None, object()],
87+
)
88+
def test_convert_invalid_inputs_raise(temperature):
89+
"""Test conversion functions with invalid inputs.
90+
91+
Args:
92+
temperature: Invalid input temperature types (list, dict, None, object).
93+
94+
Raises:
95+
TemperatureFormatNotSupported: For each invalid input type.
96+
"""
97+
98+
for fn in (convert_farenheit_to_celsius, convert_celsius_to_farenheit):
99+
with pytest.raises(TemperatureFormatNotSupported):
100+
fn(temperature)

utilities/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Utilities package for project.
2+
3+
This package exposes utility modules used by tests and application.
4+
"""
5+
6+
__all__ = ["util"]

0 commit comments

Comments
 (0)