elytra_client/DEVELOPMENT.md
claudi 459838b2e6 Add comprehensive development and style guides for Elytra PIM Client
- Introduced .copilot-instructions.md for GitHub Copilot usage guidelines.
- Created DEVELOPMENT.md detailing environment setup, workflow, and common tasks.
- Established STYLE_GUIDE.md as a quick reference for code style, formatting, and conventions.
2026-02-20 09:14:40 +01:00

617 lines
15 KiB
Markdown

# Development Guide for Elytra PIM Client
This guide provides comprehensive instructions for developers working on the Elytra PIM Client project.
## Table of Contents
1. [Environment Setup](#environment-setup)
2. [Development Workflow](#development-workflow)
3. [Code Standards](#code-standards)
4. [Testing](#testing)
5. [Common Tasks](#common-tasks)
6. [Troubleshooting](#troubleshooting)
## Environment Setup
### Prerequisites
- Python 3.9 or higher
- pip 20.0 or higher
- Git
### Initial Setup
1. **Clone the repository**
```bash
git clone https://git.him-tools.de/HIM-public/elytra_client.git
cd elytra_client
```
2. **Create virtual environment**
```bash
python -m venv .venv
```
3. **Activate virtual environment**
**Windows (PowerShell):**
```powershell
.\.venv\Scripts\Activate.ps1
```
**Windows (Command Prompt):**
```cmd
.\.venv\Scripts\activate.bat
```
**macOS/Linux:**
```bash
source .venv/bin/activate
```
4. **Install project with development dependencies**
```bash
pip install -e ".[dev]"
```
5. **Verify installation**
```bash
pytest tests/ -v
```
### Environment Configuration
1. **Copy example environment file**
```bash
cp .env.example .env
```
2. **Edit `.env` with your settings**
```
ELYTRA_API_URL=https://your-api-url/api/v1
ELYTRA_API_KEY=your-api-key-here
```
3. **Never commit `.env` file** (it's in `.gitignore`)
## Development Workflow
### Daily Development Cycle
1. **Start your work session**
```bash
# Activate virtual environment
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS/Linux
# Create a feature branch
git checkout -b feature/your-feature-name
```
2. **Write tests first (TDD)**
```bash
# Create test in tests/test_client.py or new test file
# Implement your feature
# Run tests frequently
pytest tests/ -v
```
3. **Before commits: Run all checks**
```bash
# Auto-format code
black elytra_client tests
# Organize imports
isort elytra_client tests
# Lint code
flake8 elytra_client tests
# Type check
mypy elytra_client
# Run tests
pytest tests/ -v --cov=elytra_client
```
4. **Commit your changes**
```bash
git add .
git commit -m "feat: add new feature description"
```
5. **Push and create pull request**
```bash
git push origin feature/your-feature-name
```
### Quick Workflow Commands
For convenience, run all checks in one command:
```bash
# Complete pre-commit check
black elytra_client tests && isort elytra_client tests && flake8 elytra_client tests && mypy elytra_client && pytest tests/ -v
```
Or create an alias:
```bash
# Add to .bashrc, .zshrc, or PowerShell profile
alias check-all='black elytra_client tests && isort elytra_client tests && flake8 elytra_client tests && mypy elytra_client && pytest tests/ -v'
# Then just run:
check-all
```
## Code Standards
### Language: English Only
- **All code must be in English**
- **All documentation must be in English**
- **All comments must be in English**
- **All variable/function/class names must be in English**
### Style Guide
#### Line Length
- Maximum 100 characters
- Enforced by Black formatter
- Configure in editor: View → Toggle Rulers
#### Imports
- Group: Standard library → Third-party → Local
- Use absolute imports
- Sort with isort (Black-compatible)
#### Type Hints
```python
# Good
def get_product(product_id: str) -> ProductResponse:
"""Get a product by ID."""
pass
# Bad
def get_product(product_id):
return response
# Good - with Optional
from typing import Optional
def find_product(name: Optional[str] = None) -> Optional[ProductResponse]:
"""Find product by name."""
pass
# Good - with Union (Python 3.10+)
def process_response(data: dict | list) -> str:
"""Process response data."""
pass
```
#### Docstrings (Google Style)
```python
def create_product(
name: str,
description: str,
lang: str = "en",
) -> SingleProductResponse:
"""Create a new product.
Creates a new product in the Elytra PIM system with the provided
information. The product will be created with the specified language.
Args:
name: Product name (required)
description: Product description (required)
lang: Language code for the product (default: "en")
Returns:
SingleProductResponse: The created product with assigned ID
Raises:
ElytraAuthenticationError: If authentication fails
ElytraValidationError: If required fields are missing
ElytraAPIError: For other API errors
Example:
>>> client = ElytraClient(base_url="...", api_key="...")
>>> product = client.create_product("Widget", "A useful widget")
>>> print(product.id)
"""
pass
```
#### Error Handling
```python
# Good - specific exceptions with context
from .exceptions import ElytraAPIError
try:
response = requests.get(url, timeout=self.timeout)
response.raise_for_status()
except requests.HTTPError as e:
status_code = e.response.status_code
if status_code == 404:
raise ElytraNotFoundError("Resource not found", status_code) from e
raise ElytraAPIError(f"API error: {status_code}", status_code) from e
except requests.RequestException as e:
raise ElytraAPIError(f"Network error: {str(e)}") from e
# Bad - too broad exception handling
try:
response = requests.get(url)
except:
print("Error occurred")
```
#### Naming Conventions
```python
# Constants: UPPER_CASE
API_TIMEOUT = 30
MAX_RETRIES = 3
# Classes: PascalCase
class ElytraClient:
pass
class ProductResponse:
pass
# Functions/Methods: snake_case
def get_products(self):
pass
def create_new_product(self):
pass
# Private methods: _leading_underscore
def _make_request(self):
pass
def _handle_error(self, error):
pass
# Variables: snake_case
product_id = "123"
api_key = "secret"
response_data = {}
```
## Testing
### Test Structure
```
tests/
├── test_client.py # Client functionality tests
├── test_models.py # Model validation tests (if needed)
├── test_exceptions.py # Exception handling tests (if needed)
└── conftest.py # Pytest fixtures
```
### Writing Tests
```python
import pytest
from unittest.mock import Mock, patch
from elytra_client import ElytraClient
from elytra_client.exceptions import ElytraAPIError
class TestElytraClient:
"""Tests for ElytraClient."""
@pytest.fixture
def client(self):
"""Create a test client."""
return ElytraClient(
base_url="https://api.example.com",
api_key="test-key-123"
)
def test_get_products_success(self, client):
"""Test successful product retrieval."""
with patch.object(client.session, 'request') as mock_request:
# Setup mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"items": [
{"id": "1", "name": "Product 1"},
{"id": "2", "name": "Product 2"}
]
}
mock_request.return_value = mock_response
# Execute
result = client.get_products()
# Assert
assert isinstance(result, dict)
assert "items" in result
assert len(result["items"]) == 2
def test_get_products_not_found(self, client):
"""Test product retrieval when not found."""
with patch.object(client.session, 'request') as mock_request:
# Setup mock for 404
mock_request.side_effect = Mock(
side_effect=requests.HTTPError("404 Not Found")
)
mock_request.return_value.status_code = 404
# Assert exception is raised
with pytest.raises(ElytraNotFoundError):
client.get_products()
def test_authentication_error(self, client):
"""Test authentication error handling."""
with patch.object(client.session, 'request') as mock_request:
mock_request.side_effect = requests.HTTPError("401 Unauthorized")
with pytest.raises(ElytraAuthenticationError):
client.get_products()
```
### Running Tests
```bash
# Run all tests
pytest tests/ -v
# Run with coverage report
pytest tests/ -v --cov=elytra_client --cov-report=html
# Run specific test file
pytest tests/test_client.py -v
# Run specific test class
pytest tests/test_client.py::TestElytraClient -v
# Run specific test function
pytest tests/test_client.py::TestElytraClient::test_get_products_success -v
# Run tests matching pattern
pytest tests/ -k "test_get" -v
# Run with minimal output
pytest tests/ -q
# Run with detailed output (show local variables on failure)
pytest tests/ -vv
```
### Test Coverage
```bash
# Generate HTML coverage report
pytest tests/ --cov=elytra_client --cov-report=html
# Open the report
# Open htmlcov/index.html in your browser
# Target minimum coverage
pytest tests/ --cov=elytra_client --cov-fail-under=80
```
## Common Tasks
### Adding a New API Method
1. **Add test first** in `tests/test_client.py`:
```python
def test_get_product_by_id(self, client):
"""Test getting a product by ID."""
with patch.object(client.session, 'request') as mock_request:
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "123",
"name": "Test Product"
}
mock_request.return_value = mock_response
result = client.get_product(product_id="123")
assert result.id == "123"
assert result.name == "Test Product"
```
2. **Create Pydantic model** if needed in `elytra_client/models.py`:
```python
class ProductDetailsResponse(BaseModel):
"""Response model for detailed product information."""
id: str
name: str
description: Optional[str] = None
```
3. **Implement method** in `elytra_client/client.py`:
```python
def get_product(self, product_id: str) -> ProductDetailsResponse:
"""Get product details by ID.
Args:
product_id: The product ID
Returns:
ProductDetailsResponse: The product details
Raises:
ElytraNotFoundError: If product not found
ElytraAuthenticationError: If authentication fails
"""
endpoint = f"/products/{product_id}"
return self._make_request(
method="GET",
endpoint=endpoint,
response_model=ProductDetailsResponse
)
```
4. **Run tests**:
```bash
pytest tests/test_client.py::TestElytraClient::test_get_product_by_id -v
```
5. **Run all checks**:
```bash
black elytra_client tests && isort elytra_client tests && flake8 elytra_client tests && mypy elytra_client && pytest tests/ -v
```
### Fixing a Bug
1. **Create failing test** that reproduces the bug
2. **Verify the test fails**
3. **Fix the bug**
4. **Verify the test passes**
5. **Run full test suite** to ensure no regressions
6. **Run all code checks**
7. **Commit with clear message**:
```bash
git commit -m "fix: resolve issue with product validation"
```
### Updating OpenAPI Spec
1. **Update** `openapi.yaml` (source of truth)
2. **Update models** in `models.py` to match spec
3. **Update methods** in `client.py` if endpoints changed
4. **Write/update tests**
5. **Run all checks**
### Creating a Release
1. **Update version** in `pyproject.toml`
2. **Update CHANGELOG** (if exists)
3. **Commit changes**:
```bash
git commit -m "chore: bump version to X.Y.Z"
```
4. **Tag release**:
```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```
## Troubleshooting
### Issue: Import Errors
**Problem**: `ModuleNotFoundError: No module named 'elytra_client'`
**Solution**:
```bash
# Ensure virtual environment is activated
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS/Linux
# Install package in development mode
pip install -e .
```
### Issue: Type Checking Failures
**Problem**: `error: Cannot find implementation or library stub for module named "module_name"`
**Solution**:
```bash
# Install type stubs
pip install types-requests
# Run mypy with more details
mypy elytra_client --show-error-codes
# Check specific file
mypy elytra_client/client.py
```
### Issue: Tests Fail with "Connection Error"
**Problem**: Tests are making real API calls instead of using mocks
**Solution**:
- Verify you're using `patch.object(client.session, 'request')`
- Check that mock is set up before calling client methods
- Use `mock_request.side_effect` for exceptions
```python
# Correct way to mock
with patch.object(client.session, 'request') as mock_request:
mock_request.return_value = Mock(status_code=200, ...)
# Now this won't make real request
result = client.get_products()
```
### Issue: Formatting Conflicts
**Problem**: Black and isort produce different results or conflicts
**Solution**:
```bash
# Always run in this order
black elytra_client tests
isort elytra_client tests
```
If you encounter persistent conflicts, check `.isort.cfg` or `pyproject.toml` for Black-compatible settings.
### Issue: Virtual Environment Issues
**Problem**: Changes to venv not reflected, or "python: command not found"
**Solution**:
```bash
# Deactivate current environment
deactivate
# Remove old venv
rm -rf .venv # macOS/Linux
rmdir /s .venv # Windows
# Create fresh venv
python -m venv .venv
# Activate and reinstall
.venv\Scripts\activate # Windows
pip install -e ".[dev]"
```
### Issue: Pydantic Validation Errors
**Problem**: `ValidationError: X validation error(s) for ModelName`
**Solution**:
- Check API response matches model definition in `models.py`
- Verify OpenAPI spec matches response data
- Use `response_model.model_validate(data)` to see detailed error
- Consider using `Field(default=None)` for optional fields
## VS Code Integration
### Recommended Extensions
- Python (ms-python.python)
- Pylance (ms-python.vscode-pylance)
- Black Formatter (ms-python.black-formatter)
- Flake8 (ms-python.flake8)
- Mypy Type Checker (ms-python.mypy-type-checker)
- Prettier (esbenp.prettier-vscode)
### Keyboard Shortcuts
- Format document: `Shift+Alt+F`
- Sort imports: Run command palette `isort: sort imports`
- Run tests: `Ctrl+; Ctrl+T` (with Python extension)
- Quick fix: `Ctrl+.`
### Command Palette Commands
- `Python: Create Terminal` - Creates Python terminal with venv activated
- `Test: Run All Tests` - Runs all tests
- `Python: Run Selection/Line in Python Terminal` - Test code snippets
## Additional Resources
- **Project README**: [README.md](README.md)
- **Copilot Instructions**: [.copilot-instructions.md](.copilot-instructions.md)
- **Agent Instructions**: [.agent-instructions.md](.agent-instructions.md)
- **Pydantic Docs**: https://docs.pydantic.dev/latest/
- **Pytest Docs**: https://docs.pytest.org/
- **Type Hints Docs**: https://docs.python.org/3/library/typing.html