npv2k1 commited on
Commit
eee346e
·
1 Parent(s): 9a232cd
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "python.testing.pytestArgs": ["tests"],
3
+ "python.testing.unittestEnabled": false,
4
+ "python.testing.pytestEnabled": true
5
+ }
Dockerfile CHANGED
@@ -1,25 +1,45 @@
1
  FROM python:3.11-slim AS base
2
- ENV LANG=C.UTF-8 \
3
- LC_ALL=C.UTF-8 \
4
- PYTHONDONTWRITEBYTECODE=1 \
5
- PYTHONFAULTHANDLER=1
 
 
 
 
 
6
  RUN apt-get update && \
7
- apt-get install -y --no-install-recommends gcc && \
8
- apt-get install -y ffmpeg libsm6 libxext6 && \
9
- apt-get install -y zbar-tools && \
10
- apt-get install -y libzbar-dev
11
- RUN pip install --upgrade pip
 
12
 
13
  FROM base AS builder
 
 
14
  RUN python -m venv /.venv
15
  ENV PATH="/.venv/bin:$PATH"
16
- COPY requirements.txt .
17
- RUN pip install -r requirements.txt
 
 
18
 
19
  FROM base as runtime
 
 
20
  WORKDIR /app
 
 
21
  COPY --from=builder /.venv /.venv
22
  ENV PATH="/.venv/bin:$PATH"
23
- COPY . /app
 
 
 
 
24
  EXPOSE 8000
 
 
25
  CMD ["python", "main.py"]
 
1
  FROM python:3.11-slim AS base
2
+
3
+ # Set Python environment variables
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1 \
6
+ PYTHONFAULTHANDLER=1 \
7
+ LANG=C.UTF-8 \
8
+ LC_ALL=C.UTF-8
9
+
10
+ # Install basic dependencies
11
  RUN apt-get update && \
12
+ apt-get install -y --no-install-recommends \
13
+ gcc \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Upgrade pip
17
+ RUN pip install --no-cache-dir --upgrade pip
18
 
19
  FROM base AS builder
20
+
21
+ # Create and activate virtual environment
22
  RUN python -m venv /.venv
23
  ENV PATH="/.venv/bin:$PATH"
24
+
25
+ # Install dependencies
26
+ COPY pyproject.toml .
27
+ RUN pip install --no-cache-dir .
28
 
29
  FROM base as runtime
30
+
31
+ # Set working directory
32
  WORKDIR /app
33
+
34
+ # Copy virtual environment from builder
35
  COPY --from=builder /.venv /.venv
36
  ENV PATH="/.venv/bin:$PATH"
37
+
38
+ # Copy application code
39
+ COPY . .
40
+
41
+ # Expose port (adjust if needed)
42
  EXPOSE 8000
43
+
44
+ # Run the application
45
  CMD ["python", "main.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Your Name or Organization
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Makefile CHANGED
@@ -1,6 +1,41 @@
1
- package:
2
- pip freeze > requirements.txt
3
- venv:
4
- source ./venv/bin/activate
5
- build:
6
- docker build -t template-python .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: help install dev-install test format lint type-check clean build run
2
+
3
+ help: ## Show this help menu
4
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
5
+
6
+ install: ## Install production dependencies
7
+ uv pip install .
8
+
9
+ dev-install: ## Install development dependencies
10
+ uv pip install -e ".[dev]"
11
+
12
+ test: ## Run tests with pytest
13
+ pytest -v --cov=src --cov-report=term-missing
14
+
15
+ format: ## Format code with black and isort
16
+ black .
17
+ isort .
18
+
19
+ lint: ## Lint code with ruff
20
+ ruff check .
21
+
22
+ type-check: ## Run type checking with mypy
23
+ mypy src tests
24
+
25
+ clean: ## Clean build artifacts
26
+ rm -rf build/ dist/ *.egg-info/ .coverage .pytest_cache/ .mypy_cache/ .ruff_cache/
27
+ find . -type d -name __pycache__ -exec rm -rf {} +
28
+
29
+ build: ## Build Docker image
30
+ docker build -t template-python .
31
+
32
+ run: ## Run Docker container
33
+ docker run -it --rm template-python
34
+
35
+ package: ## Create requirements.txt
36
+ uv pip freeze > requirements.txt
37
+
38
+ setup: ## Initial project setup
39
+ uv venv
40
+ $(MAKE) dev-install
41
+ cp .env.example .env
README.md CHANGED
@@ -1,2 +1,109 @@
1
- # template-python
2
- Template python project
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Project Template
2
+
3
+ A modern Python project template with best practices for development, testing, and deployment.
4
+
5
+ ## Features
6
+
7
+ - Modern Python (3.11+) project structure
8
+ - Development tools configuration (pytest, black, isort, mypy, ruff)
9
+ - Docker support
10
+ - GitHub Actions ready
11
+ - Comprehensive documentation structure
12
+ - Jupyter notebook support
13
+
14
+ ## Project Structure
15
+
16
+ ```
17
+ .
18
+ ├── docs/ # Documentation files
19
+ ├── notebooks/ # Jupyter notebooks
20
+ ├── src/ # Source code
21
+ │ ├── common/ # Common utilities and shared code
22
+ │ ├── modules/ # Feature modules
23
+ │ │ └── api/ # API related code
24
+ │ ├── shared/ # Shared resources
25
+ │ └── utils/ # Utility functions
26
+ └── tests/ # Test files
27
+ ```
28
+
29
+ ## Getting Started
30
+
31
+ ### Prerequisites
32
+
33
+ - Python 3.11 or higher
34
+ - [uv](https://github.com/astral-sh/uv) for dependency management
35
+
36
+ ### Installation
37
+
38
+ 1. Clone the repository:
39
+ ```bash
40
+ git clone https://github.com/yourusername/template-python.git
41
+ cd template-python
42
+ ```
43
+
44
+ 2. Create a virtual environment and install dependencies:
45
+ ```bash
46
+ uv venv
47
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
48
+ uv pip install -e ".[dev]"
49
+ ```
50
+
51
+ 3. Copy the environment file and adjust as needed:
52
+ ```bash
53
+ cp .env.example .env
54
+ ```
55
+
56
+ ### Development
57
+
58
+ This project uses several development tools:
59
+
60
+ - **pytest**: Testing framework
61
+ - **black**: Code formatting
62
+ - **isort**: Import sorting
63
+ - **mypy**: Static type checking
64
+ - **ruff**: Fast Python linter
65
+
66
+ Run tests:
67
+ ```bash
68
+ pytest
69
+ ```
70
+
71
+ Format code:
72
+ ```bash
73
+ black .
74
+ isort .
75
+ ```
76
+
77
+ Run type checking:
78
+ ```bash
79
+ mypy src tests
80
+ ```
81
+
82
+ Run linting:
83
+ ```bash
84
+ ruff check .
85
+ ```
86
+
87
+ ### Docker
88
+
89
+ Build the Docker image:
90
+ ```bash
91
+ make build
92
+ ```
93
+
94
+ Run the container:
95
+ ```bash
96
+ make run
97
+ ```
98
+
99
+ ## Contributing
100
+
101
+ 1. Fork the repository
102
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
103
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
104
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
105
+ 5. Open a Pull Request
106
+
107
+ ## License
108
+
109
+ This project is licensed under the MIT License - see the LICENSE file for details.
docs/README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Documentation
2
+
3
+ Welcome to the documentation for the Python Template Project.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Getting Started](getting-started.md)
8
+ 2. [Project Structure](project-structure.md)
9
+ 3. [Development Guide](development.md)
10
+ 4. [API Documentation](api.md)
11
+ 5. [Testing Guide](testing.md)
12
+ 6. [Deployment Guide](deployment.md)
13
+
14
+ ## Overview
15
+
16
+ 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)
28
+ - [Development Setup](development.md#setup)
29
+ - [Running Tests](testing.md#running-tests)
30
+ - [Docker Guide](deployment.md#docker)
31
+ - [API Reference](api.md#endpoints)
32
+
33
+ ## Contributing
34
+
35
+ See our [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to this project.
36
+
37
+ ## License
38
+
39
+ This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details.
main.py CHANGED
@@ -1 +1,59 @@
1
- print("Hello, World!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ # Configure logging
9
+ logging.basicConfig(
10
+ level=logging.INFO,
11
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
12
+ )
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def setup_environment() -> dict[str, str]:
17
+ """Load environment variables from .env file."""
18
+ env_vars = {}
19
+ env_path = Path(".env")
20
+
21
+ if env_path.exists():
22
+ with env_path.open() as f:
23
+ for line in f:
24
+ line = line.strip()
25
+ if line and not line.startswith("#"):
26
+ key, value = line.split("=", 1)
27
+ env_vars[key.strip()] = value.strip()
28
+
29
+ return env_vars
30
+
31
+
32
+ def main() -> None:
33
+ """Main application entry point."""
34
+ try:
35
+ # Load environment variables
36
+ env_vars = setup_environment()
37
+ app_name = env_vars.get("APP_NAME", "template-python")
38
+ env = env_vars.get("ENV", "development")
39
+ debug = env_vars.get("DEBUG", "false").lower() == "true"
40
+
41
+ # Configure logging level based on debug setting
42
+ if debug:
43
+ logging.getLogger().setLevel(logging.DEBUG)
44
+ logger.debug("Debug mode enabled")
45
+
46
+ logger.info(f"Starting {app_name} in {env} environment")
47
+
48
+ # Your application initialization code here
49
+ # For example:
50
+ # app = create_app()
51
+ # app.run()
52
+
53
+ except Exception as e:
54
+ logger.error(f"Application failed to start: {e}", exc_info=True)
55
+ raise
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
notebooks/notes.ipynb CHANGED
@@ -9,6 +9,17 @@
9
  "import os"
10
  ]
11
  },
 
 
 
 
 
 
 
 
 
 
 
12
  {
13
  "cell_type": "code",
14
  "execution_count": 3,
@@ -28,6 +39,32 @@
28
  "source": [
29
  "os.getpid()"
30
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
  ],
33
  "metadata": {
 
9
  "import os"
10
  ]
11
  },
12
+ {
13
+ "cell_type": "code",
14
+ "execution_count": 2,
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "# Import the parent directory into the path\n",
19
+ "import sys\n",
20
+ "sys.path.append(\"..\")"
21
+ ]
22
+ },
23
  {
24
  "cell_type": "code",
25
  "execution_count": 3,
 
39
  "source": [
40
  "os.getpid()"
41
  ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": 3,
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "from src.utils.math import hello"
50
+ ]
51
+ },
52
+ {
53
+ "cell_type": "code",
54
+ "execution_count": 4,
55
+ "metadata": {},
56
+ "outputs": [
57
+ {
58
+ "name": "stdout",
59
+ "output_type": "stream",
60
+ "text": [
61
+ "Hello from math module\n"
62
+ ]
63
+ }
64
+ ],
65
+ "source": [
66
+ "hello()"
67
+ ]
68
  }
69
  ],
70
  "metadata": {
pyproject.toml CHANGED
@@ -7,4 +7,5 @@ requires-python = ">=3.11"
7
  dependencies = [
8
  "ipykernel>=6.29.5",
9
  "numpy>=2.2.3",
 
10
  ]
 
7
  dependencies = [
8
  "ipykernel>=6.29.5",
9
  "numpy>=2.2.3",
10
+ "pytest>=8.3.4",
11
  ]
src/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """
2
+ Main application package.
3
+ This is the root package of the application.
4
+ """
src/common/__init__.py ADDED
File without changes
src/common/config.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Common configuration utilities."""
2
+
3
+ import json
4
+ import os
5
+ 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
+ """
24
+ return cls(
25
+ app_name=os.getenv("APP_NAME", "template-python"),
26
+ environment=os.getenv("ENV", "development"),
27
+ debug=os.getenv("DEBUG", "false").lower() == "true",
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
+ """
63
+ # Check common configuration locations
64
+ locations = [
65
+ Path("config"), # Project config directory
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"
src/modules/__init__.py ADDED
File without changes
src/modules/api/__init__.py ADDED
File without changes
src/modules/api/routes.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """API route definitions."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional
5
+
6
+ @dataclass
7
+ class RouteConfig:
8
+ """Configuration for an API route."""
9
+ path: str
10
+ methods: List[str]
11
+ description: str
12
+ version: Optional[str] = "v1"
13
+
14
+ # Example route configurations
15
+ ROUTES = [
16
+ RouteConfig(
17
+ path="/api/v1/health",
18
+ methods=["GET"],
19
+ description="Health check endpoint",
20
+ ),
21
+ RouteConfig(
22
+ path="/api/v1/metrics",
23
+ methods=["GET"],
24
+ description="Application metrics endpoint",
25
+ ),
26
+ ]
27
+
28
+ def get_route_config(path: str) -> Optional[RouteConfig]:
29
+ """Get route configuration by path.
30
+
31
+ Args:
32
+ path: The API route path
33
+
34
+ Returns:
35
+ Optional[RouteConfig]: The route configuration if found, None otherwise
36
+ """
37
+ return next((route for route in ROUTES if route.path == path), None)
38
+
39
+ def list_routes() -> List[RouteConfig]:
40
+ """List all available API routes.
41
+
42
+ Returns:
43
+ List[RouteConfig]: List of all route configurations
44
+ """
45
+ return ROUTES
src/shared/__init__.py ADDED
File without changes
src/shared/utils/__init__.py ADDED
File without changes
src/shared/utils/validation.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared validation utilities."""
2
+
3
+ import re
4
+ 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(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
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
+ def validate_required_fields(data: dict[str, Any], required_fields: list[str]) -> tuple[bool, Optional[str]]:
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
src/utils/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """
2
+ Main application package.
3
+ This is the root package of the application.
4
+ """
src/utils/math.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mathematical utility functions."""
2
+
3
+ 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]
tests/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """
2
+ Test package.
3
+ Contains all test cases for the application.
4
+ """
tests/test_math.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for mathematical utility functions."""
2
+
3
+ import pytest
4
+ from src.utils.math import calculate_mean, calculate_median
5
+
6
+ def test_calculate_mean():
7
+ """Test the calculate_mean function with various inputs."""
8
+ # Test with integers
9
+ assert calculate_mean([1, 2, 3, 4, 5]) == 3.0
10
+
11
+ # Test with floats
12
+ assert calculate_mean([1.5, 2.5, 3.5]) == 2.5
13
+
14
+ # Test with mixed numbers
15
+ assert calculate_mean([1, 2.5, 3]) == 2.1666666666666665
16
+
17
+ def test_calculate_mean_errors():
18
+ """Test error handling in calculate_mean function."""
19
+ # Test empty list
20
+ with pytest.raises(ValueError) as exc:
21
+ calculate_mean([])
22
+ assert str(exc.value) == "Cannot calculate mean of empty list"
23
+
24
+ # Test invalid input types
25
+ with pytest.raises(TypeError) as exc:
26
+ calculate_mean([1, "2", 3])
27
+ assert str(exc.value) == "All elements must be numbers"
28
+
29
+ def test_calculate_median():
30
+ """Test the calculate_median function with various inputs."""
31
+ # Test odd number of integers
32
+ assert calculate_median([1, 2, 3, 4, 5]) == 3.0
33
+
34
+ # Test even number of integers
35
+ assert calculate_median([1, 2, 3, 4]) == 2.5
36
+
37
+ # Test unsorted list
38
+ assert calculate_median([5, 2, 1, 4, 3]) == 3.0
39
+
40
+ # Test with floats
41
+ assert calculate_median([1.5, 2.5, 3.5]) == 2.5
42
+
43
+ # Test with mixed numbers
44
+ assert calculate_median([1, 2.5, 3]) == 2.5
45
+
46
+ def test_calculate_median_errors():
47
+ """Test error handling in calculate_median function."""
48
+ # Test empty list
49
+ with pytest.raises(ValueError) as exc:
50
+ calculate_median([])
51
+ assert str(exc.value) == "Cannot calculate median of empty list"
52
+
53
+ # Test invalid input types
54
+ with pytest.raises(TypeError) as exc:
55
+ calculate_median([1, "2", 3])
56
+ assert str(exc.value) == "All elements must be numbers"
uv.lock CHANGED
@@ -124,6 +124,15 @@ wheels = [
124
  { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 },
125
  ]
126
 
 
 
 
 
 
 
 
 
 
127
  [[package]]
128
  name = "ipykernel"
129
  version = "6.29.5"
@@ -319,6 +328,15 @@ wheels = [
319
  { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
320
  ]
321
 
 
 
 
 
 
 
 
 
 
322
  [[package]]
323
  name = "prompt-toolkit"
324
  version = "3.0.50"
@@ -382,6 +400,21 @@ wheels = [
382
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
383
  ]
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  [[package]]
386
  name = "python-dateutil"
387
  version = "2.9.0.post0"
@@ -496,12 +529,14 @@ source = { virtual = "." }
496
  dependencies = [
497
  { name = "ipykernel" },
498
  { name = "numpy" },
 
499
  ]
500
 
501
  [package.metadata]
502
  requires-dist = [
503
  { name = "ipykernel", specifier = ">=6.29.5" },
504
  { name = "numpy", specifier = ">=2.2.3" },
 
505
  ]
506
 
507
  [[package]]
 
124
  { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 },
125
  ]
126
 
127
+ [[package]]
128
+ name = "iniconfig"
129
+ version = "2.0.0"
130
+ source = { registry = "https://pypi.org/simple" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
132
+ wheels = [
133
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
134
+ ]
135
+
136
  [[package]]
137
  name = "ipykernel"
138
  version = "6.29.5"
 
328
  { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
329
  ]
330
 
331
+ [[package]]
332
+ name = "pluggy"
333
+ version = "1.5.0"
334
+ source = { registry = "https://pypi.org/simple" }
335
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
336
+ wheels = [
337
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
338
+ ]
339
+
340
  [[package]]
341
  name = "prompt-toolkit"
342
  version = "3.0.50"
 
400
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
401
  ]
402
 
403
+ [[package]]
404
+ name = "pytest"
405
+ version = "8.3.4"
406
+ source = { registry = "https://pypi.org/simple" }
407
+ dependencies = [
408
+ { name = "colorama", marker = "sys_platform == 'win32'" },
409
+ { name = "iniconfig" },
410
+ { name = "packaging" },
411
+ { name = "pluggy" },
412
+ ]
413
+ sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
414
+ wheels = [
415
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
416
+ ]
417
+
418
  [[package]]
419
  name = "python-dateutil"
420
  version = "2.9.0.post0"
 
529
  dependencies = [
530
  { name = "ipykernel" },
531
  { name = "numpy" },
532
+ { name = "pytest" },
533
  ]
534
 
535
  [package.metadata]
536
  requires-dist = [
537
  { name = "ipykernel", specifier = ">=6.29.5" },
538
  { name = "numpy", specifier = ">=2.2.3" },
539
+ { name = "pytest", specifier = ">=8.3.4" },
540
  ]
541
 
542
  [[package]]