Spaces:
Runtime error
Runtime error
feat: enhance development setup with Black and Ruff configuration, update README, and remove unused loader module
Browse files- .vscode/settings.json +13 -1
- docs/README.md +21 -0
- main.py +1 -5
- pyproject.toml +27 -0
- src/__init__.py +1 -1
- src/common/config.py +17 -11
- src/shared/utils/validation.py +18 -11
- src/utils/loader.py +0 -65
- src/utils/math.py +14 -12
.vscode/settings.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
| 1 |
{
|
| 2 |
"python.testing.pytestArgs": ["tests"],
|
| 3 |
"python.testing.unittestEnabled": false,
|
| 4 |
-
"python.testing.pytestEnabled": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"python.testing.pytestArgs": ["tests"],
|
| 3 |
"python.testing.unittestEnabled": false,
|
| 4 |
+
"python.testing.pytestEnabled": true,
|
| 5 |
+
"[python]": {
|
| 6 |
+
"editor.formatOnSave": true,
|
| 7 |
+
"editor.defaultFormatter": "ms-python.black-formatter",
|
| 8 |
+
"editor.codeActionsOnSave": {
|
| 9 |
+
"source.organizeImports": "explicit",
|
| 10 |
+
"source.fixAll": "explicit"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"editor.formatOnSave": true,
|
| 14 |
+
"python.analysis.typeCheckingMode": "basic",
|
| 15 |
+
"ruff.organizeImports": true,
|
| 16 |
+
"ruff.fixAll": true
|
| 17 |
}
|
docs/README.md
CHANGED
|
@@ -17,11 +17,32 @@ This template provides a foundation for Python projects with:
|
|
| 17 |
|
| 18 |
- Modern Python project structure
|
| 19 |
- Development tooling configuration
|
|
|
|
|
|
|
|
|
|
| 20 |
- Testing framework setup
|
| 21 |
- Docker support
|
| 22 |
- Documentation templates
|
| 23 |
- CI/CD examples
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
## Quick Links
|
| 26 |
|
| 27 |
- [Installation Guide](getting-started.md#installation)
|
|
|
|
| 17 |
|
| 18 |
- Modern Python project structure
|
| 19 |
- Development tooling configuration
|
| 20 |
+
- Black for code formatting
|
| 21 |
+
- Ruff for linting and import sorting
|
| 22 |
+
- Type checking support
|
| 23 |
- Testing framework setup
|
| 24 |
- Docker support
|
| 25 |
- Documentation templates
|
| 26 |
- CI/CD examples
|
| 27 |
|
| 28 |
+
## Development Setup
|
| 29 |
+
|
| 30 |
+
### Code Quality Tools
|
| 31 |
+
|
| 32 |
+
This project uses modern Python code quality tools:
|
| 33 |
+
|
| 34 |
+
- **Black**: Code formatter that enforces a consistent style
|
| 35 |
+
- **Ruff**: Fast Python linter and import sorter
|
| 36 |
+
- Enforces PEP 8 style guide
|
| 37 |
+
- Sorts imports automatically
|
| 38 |
+
- Checks for common errors and anti-patterns
|
| 39 |
+
- Type checking enforcement
|
| 40 |
+
|
| 41 |
+
VSCode is configured to automatically:
|
| 42 |
+
- Format code on save using Black
|
| 43 |
+
- Run Ruff for linting and import sorting
|
| 44 |
+
- Provide type checking feedback
|
| 45 |
+
|
| 46 |
## Quick Links
|
| 47 |
|
| 48 |
- [Installation Guide](getting-started.md#installation)
|
main.py
CHANGED
|
@@ -6,10 +6,6 @@ from pathlib import Path
|
|
| 6 |
from typing import Dict
|
| 7 |
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
def main() -> None:
|
| 14 |
"""Main application entry point."""
|
| 15 |
try:
|
|
@@ -22,4 +18,4 @@ def main() -> None:
|
|
| 22 |
|
| 23 |
|
| 24 |
if __name__ == "__main__":
|
| 25 |
-
main()
|
|
|
|
| 6 |
from typing import Dict
|
| 7 |
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
def main() -> None:
|
| 10 |
"""Main application entry point."""
|
| 11 |
try:
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
if __name__ == "__main__":
|
| 21 |
+
main()
|
pyproject.toml
CHANGED
|
@@ -10,3 +10,30 @@ dependencies = [
|
|
| 10 |
"numpy>=2.2.3",
|
| 11 |
"pytest>=8.3.4",
|
| 12 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"numpy>=2.2.3",
|
| 11 |
"pytest>=8.3.4",
|
| 12 |
]
|
| 13 |
+
|
| 14 |
+
[tool.black]
|
| 15 |
+
line-length = 88
|
| 16 |
+
target-version = ["py311"]
|
| 17 |
+
include = '\.pyi?$'
|
| 18 |
+
|
| 19 |
+
[tool.ruff]
|
| 20 |
+
line-length = 88
|
| 21 |
+
target-version = "py311"
|
| 22 |
+
select = [
|
| 23 |
+
"E", # pycodestyle errors
|
| 24 |
+
"W", # pycodestyle warnings
|
| 25 |
+
"F", # pyflakes
|
| 26 |
+
"I", # isort
|
| 27 |
+
"C", # flake8-comprehensions
|
| 28 |
+
"B", # flake8-bugbear
|
| 29 |
+
]
|
| 30 |
+
ignore = []
|
| 31 |
+
|
| 32 |
+
[tool.ruff.per-file-ignores]
|
| 33 |
+
"__init__.py" = ["F401"]
|
| 34 |
+
|
| 35 |
+
[tool.ruff.isort]
|
| 36 |
+
known-first-party = ["src"]
|
| 37 |
+
|
| 38 |
+
[tool.ruff.mccabe]
|
| 39 |
+
max-complexity = 10
|
src/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
"""
|
| 2 |
Main application package.
|
| 3 |
This is the root package of the application.
|
| 4 |
-
"""
|
|
|
|
| 1 |
"""
|
| 2 |
Main application package.
|
| 3 |
This is the root package of the application.
|
| 4 |
+
"""
|
src/common/config.py
CHANGED
|
@@ -6,18 +6,20 @@ from dataclasses import dataclass
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Any, Optional
|
| 8 |
|
|
|
|
| 9 |
@dataclass
|
| 10 |
class AppConfig:
|
| 11 |
"""Application configuration."""
|
|
|
|
| 12 |
app_name: str
|
| 13 |
environment: str
|
| 14 |
debug: bool
|
| 15 |
log_level: str
|
| 16 |
-
|
| 17 |
@classmethod
|
| 18 |
def from_env(cls) -> "AppConfig":
|
| 19 |
"""Create configuration from environment variables.
|
| 20 |
-
|
| 21 |
Returns:
|
| 22 |
AppConfig: Application configuration instance
|
| 23 |
"""
|
|
@@ -28,35 +30,39 @@ class AppConfig:
|
|
| 28 |
log_level=os.getenv("LOG_LEVEL", "INFO"),
|
| 29 |
)
|
| 30 |
|
|
|
|
| 31 |
def load_json_config(path: Path) -> dict[str, Any]:
|
| 32 |
"""Load configuration from JSON file.
|
| 33 |
-
|
| 34 |
Args:
|
| 35 |
path: Path to JSON configuration file
|
| 36 |
-
|
| 37 |
Returns:
|
| 38 |
dict[str, Any]: Configuration dictionary
|
| 39 |
-
|
| 40 |
Raises:
|
| 41 |
FileNotFoundError: If configuration file doesn't exist
|
| 42 |
json.JSONDecodeError: If configuration file is invalid JSON
|
| 43 |
"""
|
| 44 |
if not path.exists():
|
| 45 |
raise FileNotFoundError(f"Configuration file not found: {path}")
|
| 46 |
-
|
| 47 |
with path.open() as f:
|
| 48 |
return json.load(f)
|
| 49 |
|
|
|
|
| 50 |
class ConfigurationError(Exception):
|
| 51 |
"""Base class for configuration errors."""
|
|
|
|
| 52 |
pass
|
| 53 |
|
|
|
|
| 54 |
def get_config_path(config_name: str) -> Path:
|
| 55 |
"""Get configuration file path.
|
| 56 |
-
|
| 57 |
Args:
|
| 58 |
config_name: Name of the configuration file
|
| 59 |
-
|
| 60 |
Returns:
|
| 61 |
Path: Path to configuration file
|
| 62 |
"""
|
|
@@ -66,10 +72,10 @@ def get_config_path(config_name: str) -> Path:
|
|
| 66 |
Path("~/.config/template-python"), # User config directory
|
| 67 |
Path("/etc/template-python"), # System config directory
|
| 68 |
]
|
| 69 |
-
|
| 70 |
for location in locations:
|
| 71 |
path = location.expanduser() / f"{config_name}.json"
|
| 72 |
if path.exists():
|
| 73 |
return path
|
| 74 |
-
|
| 75 |
-
return locations[0].expanduser() / f"{config_name}.json"
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Any, Optional
|
| 8 |
|
| 9 |
+
|
| 10 |
@dataclass
|
| 11 |
class AppConfig:
|
| 12 |
"""Application configuration."""
|
| 13 |
+
|
| 14 |
app_name: str
|
| 15 |
environment: str
|
| 16 |
debug: bool
|
| 17 |
log_level: str
|
| 18 |
+
|
| 19 |
@classmethod
|
| 20 |
def from_env(cls) -> "AppConfig":
|
| 21 |
"""Create configuration from environment variables.
|
| 22 |
+
|
| 23 |
Returns:
|
| 24 |
AppConfig: Application configuration instance
|
| 25 |
"""
|
|
|
|
| 30 |
log_level=os.getenv("LOG_LEVEL", "INFO"),
|
| 31 |
)
|
| 32 |
|
| 33 |
+
|
| 34 |
def load_json_config(path: Path) -> dict[str, Any]:
|
| 35 |
"""Load configuration from JSON file.
|
| 36 |
+
|
| 37 |
Args:
|
| 38 |
path: Path to JSON configuration file
|
| 39 |
+
|
| 40 |
Returns:
|
| 41 |
dict[str, Any]: Configuration dictionary
|
| 42 |
+
|
| 43 |
Raises:
|
| 44 |
FileNotFoundError: If configuration file doesn't exist
|
| 45 |
json.JSONDecodeError: If configuration file is invalid JSON
|
| 46 |
"""
|
| 47 |
if not path.exists():
|
| 48 |
raise FileNotFoundError(f"Configuration file not found: {path}")
|
| 49 |
+
|
| 50 |
with path.open() as f:
|
| 51 |
return json.load(f)
|
| 52 |
|
| 53 |
+
|
| 54 |
class ConfigurationError(Exception):
|
| 55 |
"""Base class for configuration errors."""
|
| 56 |
+
|
| 57 |
pass
|
| 58 |
|
| 59 |
+
|
| 60 |
def get_config_path(config_name: str) -> Path:
|
| 61 |
"""Get configuration file path.
|
| 62 |
+
|
| 63 |
Args:
|
| 64 |
config_name: Name of the configuration file
|
| 65 |
+
|
| 66 |
Returns:
|
| 67 |
Path: Path to configuration file
|
| 68 |
"""
|
|
|
|
| 72 |
Path("~/.config/template-python"), # User config directory
|
| 73 |
Path("/etc/template-python"), # System config directory
|
| 74 |
]
|
| 75 |
+
|
| 76 |
for location in locations:
|
| 77 |
path = location.expanduser() / f"{config_name}.json"
|
| 78 |
if path.exists():
|
| 79 |
return path
|
| 80 |
+
|
| 81 |
+
return locations[0].expanduser() / f"{config_name}.json"
|
src/shared/utils/validation.py
CHANGED
|
@@ -5,43 +5,50 @@ from typing import Any, Optional, Pattern
|
|
| 5 |
|
| 6 |
# Common validation patterns
|
| 7 |
EMAIL_PATTERN: Pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
| 8 |
-
UUID_PATTERN: Pattern = re.compile(
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def validate_email(email: str) -> bool:
|
| 11 |
"""Validate email format.
|
| 12 |
-
|
| 13 |
Args:
|
| 14 |
email: Email address to validate
|
| 15 |
-
|
| 16 |
Returns:
|
| 17 |
bool: True if email format is valid, False otherwise
|
| 18 |
"""
|
| 19 |
return bool(EMAIL_PATTERN.match(email))
|
| 20 |
|
|
|
|
| 21 |
def validate_uuid(uuid: str) -> bool:
|
| 22 |
"""Validate UUID format.
|
| 23 |
-
|
| 24 |
Args:
|
| 25 |
uuid: UUID string to validate
|
| 26 |
-
|
| 27 |
Returns:
|
| 28 |
bool: True if UUID format is valid, False otherwise
|
| 29 |
"""
|
| 30 |
return bool(UUID_PATTERN.match(uuid.lower()))
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
"""Validate required fields in a dictionary.
|
| 34 |
-
|
| 35 |
Args:
|
| 36 |
data: Dictionary containing data to validate
|
| 37 |
required_fields: List of required field names
|
| 38 |
-
|
| 39 |
Returns:
|
| 40 |
tuple[bool, Optional[str]]: (is_valid, error_message)
|
| 41 |
"""
|
| 42 |
missing_fields = [field for field in required_fields if field not in data]
|
| 43 |
-
|
| 44 |
if missing_fields:
|
| 45 |
return False, f"Missing required fields: {', '.join(missing_fields)}"
|
| 46 |
-
|
| 47 |
-
return True, None
|
|
|
|
| 5 |
|
| 6 |
# Common validation patterns
|
| 7 |
EMAIL_PATTERN: Pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
| 8 |
+
UUID_PATTERN: Pattern = re.compile(
|
| 9 |
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
|
| 13 |
def validate_email(email: str) -> bool:
|
| 14 |
"""Validate email format.
|
| 15 |
+
|
| 16 |
Args:
|
| 17 |
email: Email address to validate
|
| 18 |
+
|
| 19 |
Returns:
|
| 20 |
bool: True if email format is valid, False otherwise
|
| 21 |
"""
|
| 22 |
return bool(EMAIL_PATTERN.match(email))
|
| 23 |
|
| 24 |
+
|
| 25 |
def validate_uuid(uuid: str) -> bool:
|
| 26 |
"""Validate UUID format.
|
| 27 |
+
|
| 28 |
Args:
|
| 29 |
uuid: UUID string to validate
|
| 30 |
+
|
| 31 |
Returns:
|
| 32 |
bool: True if UUID format is valid, False otherwise
|
| 33 |
"""
|
| 34 |
return bool(UUID_PATTERN.match(uuid.lower()))
|
| 35 |
|
| 36 |
+
|
| 37 |
+
def validate_required_fields(
|
| 38 |
+
data: dict[str, Any], required_fields: list[str]
|
| 39 |
+
) -> tuple[bool, Optional[str]]:
|
| 40 |
"""Validate required fields in a dictionary.
|
| 41 |
+
|
| 42 |
Args:
|
| 43 |
data: Dictionary containing data to validate
|
| 44 |
required_fields: List of required field names
|
| 45 |
+
|
| 46 |
Returns:
|
| 47 |
tuple[bool, Optional[str]]: (is_valid, error_message)
|
| 48 |
"""
|
| 49 |
missing_fields = [field for field in required_fields if field not in data]
|
| 50 |
+
|
| 51 |
if missing_fields:
|
| 52 |
return False, f"Missing required fields: {', '.join(missing_fields)}"
|
| 53 |
+
|
| 54 |
+
return True, None
|
src/utils/loader.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
"""Module loader utilities."""
|
| 2 |
-
|
| 3 |
-
import logging
|
| 4 |
-
import uvicorn
|
| 5 |
-
from typing import Any, Optional
|
| 6 |
-
|
| 7 |
-
from src.modules.api.app import app as api_app
|
| 8 |
-
from src.modules.api.core.config import settings
|
| 9 |
-
|
| 10 |
-
logger = logging.getLogger(__name__)
|
| 11 |
-
|
| 12 |
-
class ModuleLoader:
|
| 13 |
-
"""Utility class for loading and running modules."""
|
| 14 |
-
|
| 15 |
-
@staticmethod
|
| 16 |
-
def load_api(
|
| 17 |
-
host: str = "0.0.0.0",
|
| 18 |
-
port: int = 8000,
|
| 19 |
-
reload: bool = False,
|
| 20 |
-
**kwargs: Any
|
| 21 |
-
) -> None:
|
| 22 |
-
"""Load and run the API module.
|
| 23 |
-
|
| 24 |
-
Args:
|
| 25 |
-
host: Host to bind the server to
|
| 26 |
-
port: Port to bind the server to
|
| 27 |
-
reload: Enable auto-reload for development
|
| 28 |
-
**kwargs: Additional uvicorn configuration
|
| 29 |
-
"""
|
| 30 |
-
logger.info(f"Starting API server on {host}:{port}")
|
| 31 |
-
logger.debug(f"API Settings: {settings.dict()}")
|
| 32 |
-
|
| 33 |
-
config = uvicorn.Config(
|
| 34 |
-
api_app,
|
| 35 |
-
host=host,
|
| 36 |
-
port=port,
|
| 37 |
-
reload=reload,
|
| 38 |
-
log_level="debug" if settings.debug else "info",
|
| 39 |
-
**kwargs
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
server = uvicorn.Server(config)
|
| 43 |
-
server.run()
|
| 44 |
-
|
| 45 |
-
@classmethod
|
| 46 |
-
def load_module(
|
| 47 |
-
cls,
|
| 48 |
-
module_name: str,
|
| 49 |
-
**kwargs: Any
|
| 50 |
-
) -> Optional[Any]:
|
| 51 |
-
"""Generic module loader.
|
| 52 |
-
|
| 53 |
-
Args:
|
| 54 |
-
module_name: Name of the module to load
|
| 55 |
-
**kwargs: Module-specific configuration
|
| 56 |
-
|
| 57 |
-
Returns:
|
| 58 |
-
Optional[Any]: Module instance if applicable
|
| 59 |
-
"""
|
| 60 |
-
if module_name == "api":
|
| 61 |
-
cls.load_api(**kwargs)
|
| 62 |
-
return api_app
|
| 63 |
-
else:
|
| 64 |
-
logger.error(f"Unknown module: {module_name}")
|
| 65 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/math.py
CHANGED
|
@@ -4,51 +4,53 @@ from typing import List, Union
|
|
| 4 |
|
| 5 |
Number = Union[int, float]
|
| 6 |
|
|
|
|
| 7 |
def calculate_mean(numbers: List[Number]) -> float:
|
| 8 |
"""Calculate the arithmetic mean of a list of numbers.
|
| 9 |
-
|
| 10 |
Args:
|
| 11 |
numbers: A list of integers or floating point numbers
|
| 12 |
-
|
| 13 |
Returns:
|
| 14 |
float: The arithmetic mean of the input numbers
|
| 15 |
-
|
| 16 |
Raises:
|
| 17 |
ValueError: If the input list is empty
|
| 18 |
TypeError: If any element is not a number
|
| 19 |
"""
|
| 20 |
if not numbers:
|
| 21 |
raise ValueError("Cannot calculate mean of empty list")
|
| 22 |
-
|
| 23 |
if not all(isinstance(x, (int, float)) for x in numbers):
|
| 24 |
raise TypeError("All elements must be numbers")
|
| 25 |
-
|
| 26 |
return sum(numbers) / len(numbers)
|
| 27 |
|
|
|
|
| 28 |
def calculate_median(numbers: List[Number]) -> float:
|
| 29 |
"""Calculate the median value from a list of numbers.
|
| 30 |
-
|
| 31 |
Args:
|
| 32 |
numbers: A list of integers or floating point numbers
|
| 33 |
-
|
| 34 |
Returns:
|
| 35 |
float: The median value of the input numbers
|
| 36 |
-
|
| 37 |
Raises:
|
| 38 |
ValueError: If the input list is empty
|
| 39 |
TypeError: If any element is not a number
|
| 40 |
"""
|
| 41 |
if not numbers:
|
| 42 |
raise ValueError("Cannot calculate median of empty list")
|
| 43 |
-
|
| 44 |
if not all(isinstance(x, (int, float)) for x in numbers):
|
| 45 |
raise TypeError("All elements must be numbers")
|
| 46 |
-
|
| 47 |
sorted_numbers = sorted(numbers)
|
| 48 |
length = len(sorted_numbers)
|
| 49 |
-
|
| 50 |
if length % 2 == 0:
|
| 51 |
mid = length // 2
|
| 52 |
return (sorted_numbers[mid - 1] + sorted_numbers[mid]) / 2
|
| 53 |
else:
|
| 54 |
-
return sorted_numbers[length // 2]
|
|
|
|
| 4 |
|
| 5 |
Number = Union[int, float]
|
| 6 |
|
| 7 |
+
|
| 8 |
def calculate_mean(numbers: List[Number]) -> float:
|
| 9 |
"""Calculate the arithmetic mean of a list of numbers.
|
| 10 |
+
|
| 11 |
Args:
|
| 12 |
numbers: A list of integers or floating point numbers
|
| 13 |
+
|
| 14 |
Returns:
|
| 15 |
float: The arithmetic mean of the input numbers
|
| 16 |
+
|
| 17 |
Raises:
|
| 18 |
ValueError: If the input list is empty
|
| 19 |
TypeError: If any element is not a number
|
| 20 |
"""
|
| 21 |
if not numbers:
|
| 22 |
raise ValueError("Cannot calculate mean of empty list")
|
| 23 |
+
|
| 24 |
if not all(isinstance(x, (int, float)) for x in numbers):
|
| 25 |
raise TypeError("All elements must be numbers")
|
| 26 |
+
|
| 27 |
return sum(numbers) / len(numbers)
|
| 28 |
|
| 29 |
+
|
| 30 |
def calculate_median(numbers: List[Number]) -> float:
|
| 31 |
"""Calculate the median value from a list of numbers.
|
| 32 |
+
|
| 33 |
Args:
|
| 34 |
numbers: A list of integers or floating point numbers
|
| 35 |
+
|
| 36 |
Returns:
|
| 37 |
float: The median value of the input numbers
|
| 38 |
+
|
| 39 |
Raises:
|
| 40 |
ValueError: If the input list is empty
|
| 41 |
TypeError: If any element is not a number
|
| 42 |
"""
|
| 43 |
if not numbers:
|
| 44 |
raise ValueError("Cannot calculate median of empty list")
|
| 45 |
+
|
| 46 |
if not all(isinstance(x, (int, float)) for x in numbers):
|
| 47 |
raise TypeError("All elements must be numbers")
|
| 48 |
+
|
| 49 |
sorted_numbers = sorted(numbers)
|
| 50 |
length = len(sorted_numbers)
|
| 51 |
+
|
| 52 |
if length % 2 == 0:
|
| 53 |
mid = length // 2
|
| 54 |
return (sorted_numbers[mid - 1] + sorted_numbers[mid]) / 2
|
| 55 |
else:
|
| 56 |
+
return sorted_numbers[length // 2]
|