diff --git a/.gitignore b/.gitignore index f61b850..3b7cc3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,48 +1,46 @@ -.venv -.env -__pycache__ -.pytest_cache -.pypirc -*.db -test -test_state.json -task_flow.egg-info -example_repo -signature.js -git-filter-repo -task/orca/ -**/dist/ -# yarn.lock -package-lock.json -node_modules -build -migrate.sh -*/dev.js -executables/* -namespace/* -config/* -.env.local -taskStateInfoKeypair.json -localKOIIDB.db -metadata.json -.npmrc -*.pem -.vscode -.cursor -data/chunks -data/process -test_state.csv -todos-example.csv - - -# Ignore auto-generated repository directories -repos/ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# Environment and configuration +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ -# Ignore Data -data/* - +# Testing +.pytest_cache/ +.coverage +htmlcov/ -venv +# IDE +.vscode/ +.idea/ +*.swp +*.swo -**/venv/ +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..43150c0 --- /dev/null +++ b/src/config.py @@ -0,0 +1,63 @@ +import os +from typing import Optional, Dict, Any +from dotenv import load_dotenv + + +class ConfigurationError(Exception): + """Custom exception for configuration loading errors.""" + pass + + +def load_config(config_file: Optional[str] = None) -> Dict[str, Any]: + """ + Load configuration from environment variables or a specified config file. + + Args: + config_file (Optional[str]): Path to an optional configuration file. + + Returns: + Dict[str, Any]: Loaded configuration dictionary. + + Raises: + ConfigurationError: If required configuration is missing or invalid. + """ + # Load .env file if specified or default .env exists + if config_file: + load_dotenv(config_file) + else: + load_dotenv() + + # Configuration dictionary to store loaded settings + config = { + 'api_base_url': os.getenv('COINGECKO_API_BASE_URL', 'https://api.coingecko.com/api/v3'), + 'api_key': os.getenv('COINGECKO_API_KEY', ''), + 'request_timeout': int(os.getenv('COINGECKO_REQUEST_TIMEOUT', 30)), + 'max_retries': int(os.getenv('COINGECKO_MAX_RETRIES', 3)), + 'rate_limit_delay': int(os.getenv('COINGECKO_RATE_LIMIT_DELAY', 1)) + } + + # Validate critical configuration + _validate_config(config) + + return config + + +def _validate_config(config: Dict[str, Any]) -> None: + """ + Validate the loaded configuration. + + Args: + config (Dict[str, Any]): Configuration dictionary to validate. + + Raises: + ConfigurationError: If configuration is invalid. + """ + # Basic validation rules + if not config['api_base_url']: + raise ConfigurationError("Invalid or missing API base URL") + + if config['request_timeout'] <= 0: + raise ConfigurationError("Request timeout must be a positive integer") + + if config['max_retries'] < 0: + raise ConfigurationError("Max retries cannot be negative") \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..0dd7ddb --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,59 @@ +import os +import pytest +from src.config import load_config, ConfigurationError + + +def test_default_config(monkeypatch): + """Test default configuration loading.""" + monkeypatch.delenv('COINGECKO_API_BASE_URL', raising=False) + monkeypatch.delenv('COINGECKO_API_KEY', raising=False) + monkeypatch.delenv('COINGECKO_REQUEST_TIMEOUT', raising=False) + monkeypatch.delenv('COINGECKO_MAX_RETRIES', raising=False) + monkeypatch.delenv('COINGECKO_RATE_LIMIT_DELAY', raising=False) + + config = load_config() + assert config['api_base_url'] == 'https://api.coingecko.com/api/v3' + assert config['api_key'] == '' + assert config['request_timeout'] == 30 + assert config['max_retries'] == 3 + assert config['rate_limit_delay'] == 1 + + +def test_custom_config(monkeypatch): + """Test loading custom configuration from environment variables.""" + monkeypatch.setenv('COINGECKO_API_BASE_URL', 'https://custom-api.com') + monkeypatch.setenv('COINGECKO_API_KEY', 'test_key') + monkeypatch.setenv('COINGECKO_REQUEST_TIMEOUT', '60') + monkeypatch.setenv('COINGECKO_MAX_RETRIES', '5') + monkeypatch.setenv('COINGECKO_RATE_LIMIT_DELAY', '2') + + config = load_config() + assert config['api_base_url'] == 'https://custom-api.com' + assert config['api_key'] == 'test_key' + assert config['request_timeout'] == 60 + assert config['max_retries'] == 5 + assert config['rate_limit_delay'] == 2 + + +def test_invalid_timeout_config(monkeypatch): + """Test configuration validation for negative timeout.""" + monkeypatch.setenv('COINGECKO_REQUEST_TIMEOUT', '-10') + + with pytest.raises(ConfigurationError, match="Request timeout must be a positive integer"): + load_config() + + +def test_empty_base_url_config(monkeypatch): + """Test configuration validation for empty base URL.""" + monkeypatch.setenv('COINGECKO_API_BASE_URL', '') + + with pytest.raises(ConfigurationError, match="Invalid or missing API base URL"): + load_config() + + +def test_negative_retries_config(monkeypatch): + """Test configuration validation for negative retries.""" + monkeypatch.setenv('COINGECKO_MAX_RETRIES', '-3') + + with pytest.raises(ConfigurationError, match="Max retries cannot be negative"): + load_config() \ No newline at end of file