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.
This commit is contained in:
parent
05fca294f9
commit
459838b2e6
4 changed files with 1794 additions and 0 deletions
617
DEVELOPMENT.md
Normal file
617
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue