feat: add production readiness improvements for AGPLv3 release

Security:
- Remove -k/--api-key CLI option (prevents process list exposure)
- API key now only accepted via NREL_API_KEY environment variable

Features:
- Add API timeout (30s) and retry logic with exponential backoff
- Add rate limit detection and graceful test skipping

Documentation:
- Add AGPLv3 LICENSE file
- Add CONTRIBUTING.md with development guidelines
- Add CHANGELOG.md following Keep a Changelog format
- Add copyright headers to all source files

Tests:
- Expand test suite from 19 to 52 tests
- Add edge case tests (negative values, boundaries)
- Add input validation tests
- Add financial calculation verification tests
- Add rate limit handling to skip tests gracefully
- Remove skip-on-failure logic - tests now properly fail

All 52 tests pass (19 skipped when API rate limited).

🤖 Generated with [Crush](https://crush.cli.software)

Assisted-by: GLM-5 via Crush <crush@charm.land>
This commit is contained in:
Charles N Wyble
2026-02-27 17:10:22 -05:00
parent 400764a9ff
commit 0bbd0fb484
8 changed files with 1393 additions and 96 deletions

View File

@@ -1,4 +1,20 @@
#!/usr/bin/env bash
#
# SITER Solar Analysis - NREL PVWatts Solar Production Estimator
# Copyright (C) 2026 Known Element
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# shellcheck source=/dev/null
#
# siter-solar-analysis - NREL PVWatts Solar Production Estimator
@@ -41,6 +57,10 @@ readonly SELF_CONSUMPTION_PCT="0.60"
# API endpoint
readonly API_URL="https://developer.nrel.gov/api/pvwatts/v6.json"
# API configuration
readonly API_TIMEOUT="30"
readonly API_MAX_RETRIES="3"
readonly API_RETRY_DELAY="2"
# Colors for output
readonly RED='\033[0;31m'
@@ -95,7 +115,6 @@ USAGE:
$(basename "$0") [OPTIONS]
OPTIONS:
-k, --api-key KEY NREL API key (or set NREL_API_KEY env var)
--lat VALUE Site latitude (default: ${DEFAULT_LAT})
--lon VALUE Site longitude (default: ${DEFAULT_LON})
-p, --panels NUM Number of panels (default: ${DEFAULT_PANELS})
@@ -113,9 +132,6 @@ EXAMPLES:
# Basic analysis with defaults
$(basename "$0")
# With your API key
$(basename "$0") -k YOUR_API_KEY
# Compare different system sizes
$(basename "$0") --scenarios
@@ -226,11 +242,30 @@ query_nrel_api() {
local response
local http_code
local retry_count=0
local delay=$API_RETRY_DELAY
# shellcheck disable=SC2086
response=$(curl -s -w "\n%{http_code}" ${url} 2>/dev/null)
http_code=$(echo "$response" | tail -n1)
response=$(echo "$response" | sed '$d')
while [[ $retry_count -lt $API_MAX_RETRIES ]]; do
# shellcheck disable=SC2086
response=$(curl -s -S --max-time "$API_TIMEOUT" -w "\n%{http_code}" ${url} 2>&1)
http_code=$(echo "$response" | tail -n1)
response=$(echo "$response" | sed '$d')
# Check for timeout or network errors
if [[ "$http_code" == "000" ]] || [[ "$response" == *"timed out"* ]] || [[ "$response" == *"connection"* ]]; then
retry_count=$((retry_count + 1))
if [[ $retry_count -lt $API_MAX_RETRIES ]]; then
warn "API request failed (attempt $retry_count/$API_MAX_RETRIES), retrying in ${delay}s..."
sleep "$delay"
delay=$((delay * 2)) # Exponential backoff
continue
fi
error "API request failed after $API_MAX_RETRIES attempts: timeout or connection error"
fi
# Success - break out of retry loop
break
done
if [[ "$http_code" != "200" ]]; then
error "API request failed with HTTP ${http_code}: ${response}"
@@ -580,10 +615,6 @@ main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-k|--api-key)
api_key="$2"
shift 2
;;
--lat)
lat="$2"
shift 2
@@ -636,9 +667,7 @@ main() {
error "Unknown option: $1\nTry --help for usage information."
;;
*)
# Positional argument - treat as API key
api_key="$1"
shift
error "Unexpected argument: $1\nTry --help for usage information."
;;
esac
done