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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user