Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 42 additions & 44 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -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))
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for invalid numeric environment variables.

The current implementation will raise a ValueError if environment variables contain non-numeric values. Consider wrapping the int conversions in try-catch blocks for better error messages.

-        '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'))
+        'request_timeout': _parse_int_env('COINGECKO_REQUEST_TIMEOUT', '30'),
+        'max_retries': _parse_int_env('COINGECKO_MAX_RETRIES', '3'),
+        'rate_limit_delay': _parse_int_env('COINGECKO_RATE_LIMIT_DELAY', '1')

Add this helper function before the load_config function:

def _parse_int_env(env_var: str, default: str) -> int:
    """Parse integer from environment variable with error handling."""
    try:
        return int(os.getenv(env_var, default))
    except ValueError as e:
        raise ConfigurationError(f"Invalid value for {env_var}: must be an integer") from e
🧰 Tools
🪛 Pylint (3.3.7)

[warning] 34-34: os.getenv default type is builtins.int. Expected str or None.

(W1508)


[warning] 35-35: os.getenv default type is builtins.int. Expected str or None.

(W1508)


[warning] 36-36: os.getenv default type is builtins.int. Expected str or None.

(W1508)

🤖 Prompt for AI Agents
In src/config.py around lines 34 to 36, the int conversions of environment
variables can raise ValueError if the values are non-numeric. To fix this,
create a helper function that wraps the int conversion in a try-except block,
catching ValueError and raising a ConfigurationError with a clear message. Then
replace the direct int conversions with calls to this helper function to ensure
robust error handling and clearer error messages.

🛠️ Refactor suggestion

Fix type safety issues with os.getenv defaults.

The current implementation passes integer defaults to os.getenv, which expects string defaults. This could cause type-related issues.

-        '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))
+        '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'))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'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))
'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'))
🧰 Tools
🪛 Pylint (3.3.7)

[warning] 34-34: os.getenv default type is builtins.int. Expected str or None.

(W1508)


[warning] 35-35: os.getenv default type is builtins.int. Expected str or None.

(W1508)


[warning] 36-36: os.getenv default type is builtins.int. Expected str or None.

(W1508)

🤖 Prompt for AI Agents
In src/config.py around lines 34 to 36, the os.getenv calls use integer
defaults, but os.getenv expects string defaults. To fix this, change the default
values to strings (e.g., '30', '3', '1') and then convert the result to int
after retrieving the environment variable. This ensures type safety and prevents
potential errors.

}

# 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")
59 changes: 59 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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()