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:
claudi 2026-02-20 09:14:40 +01:00
parent 05fca294f9
commit 459838b2e6
4 changed files with 1794 additions and 0 deletions

303
.agent-instructions.md Normal file
View file

@ -0,0 +1,303 @@
# Agent Instructions for Elytra PIM Client
## Purpose
This document provides detailed instructions for autonomous agents (automated tools, CI/CD systems, code generation tools) working with the Elytra PIM Client project.
## Project Identity
- **Name**: Elytra PIM Client
- **Description**: A fully Pythonic, Pydantic-driven client for the Elytra Product Information Management API
- **Language**: Python 3.9+
- **Type System**: Full type hints with static type checking (mypy)
- **Data Validation**: Pydantic v2
- **Package Manager**: pip with setuptools
## Mandatory Rules
### 1. Language Requirement
- **ALL code must be in English**
- **ALL docstrings must be in English**
- **ALL comments must be in English**
- **ALL variable/function/class names must be in English**
- **NO exceptions to this rule**
### 2. Code Quality Gates
- Must pass Black formatting (100 char line length)
- Must pass isort import checks
- Must pass Flake8 linting
- Must pass mypy type checking
- Must achieve >80% test coverage
- All tests must pass
### 3. Source of Truth
- **OpenAPI specification** (`openapi.yaml`) is the source of truth for API design
- All Pydantic models should reflect the OpenAPI spec
- API endpoints should match OpenAPI definitions
- Request/response models must align with OpenAPI
## Automated Task Guidelines
### Before Any Code Modification
1. Verify current state of the codebase
2. Check existing tests and coverage
3. Understand the specific change needed
4. Create a test case first if fixing a bug
5. Run full test suite to establish baseline
### When Generating Code
1. Follow Google-style docstrings strictly
2. Include comprehensive type hints
3. Add docstrings for all public APIs
4. Create corresponding tests
5. Ensure code passes all formatting tools
6. Add to appropriate module without reorganizing structure
### When Modifying Tests
1. Preserve test isolation (each test independent)
2. Use pytest fixtures for setup/teardown
3. Mock external dependencies (requests, etc.)
4. Test both success and error paths
5. Include descriptive test names: `test_<action>_<condition>_<expected_result>`
### When Running Tests
```bash
# Full test suite with coverage
pytest tests/ -v --cov=elytra_client --cov-report=term-missing
# Specific test file
pytest tests/test_client.py -v
# With markers (if defined)
pytest tests/ -v -m "not integration"
```
### When Formatting Code
Execute in this order:
```bash
1. black elytra_client tests
2. isort elytra_client tests
3. flake8 elytra_client tests
4. mypy elytra_client
5. pytest tests/ -v
```
## File Organization Rules
### Module Responsibilities
- **`client.py`**: Main `ElytraClient` class with API methods
- **`models.py`**: All Pydantic models for request/response validation
- **`exceptions.py`**: Custom exception hierarchy
- **`config.py`**: Configuration and environment setup
- **`__init__.py`**: Package-level exports
### Do Not
- ❌ Create new modules without justification
- ❌ Move code between modules without updating imports
- ❌ Reorganize directory structure
- ❌ Rename existing modules or classes
## API Method Generation Rules
When adding new API methods to `ElytraClient`:
```python
def method_name(
self,
required_param: str,
optional_param: Optional[str] = None,
lang: str = "en",
page: int = 1,
limit: int = 10,
) -> ResponseModelType | Dict[str, Any]:
"""
One-line summary.
Longer description if needed.
Args:
required_param: Description
optional_param: Description (default: None)
lang: Language code (default: "en")
page: Page number for pagination (default: 1)
limit: Items per page (default: 10)
Returns:
Pydantic model instance or dict with 'items' key for collections
Raises:
ElytraNotFoundError: If resource not found
ElytraAuthenticationError: If authentication fails
ElytraValidationError: If data validation fails
ElytraAPIError: For other API errors
"""
endpoint = f"/endpoint/{required_param}"
params = {"lang": lang, "page": page, "limit": limit}
if optional_param:
params["optional"] = optional_param
return self._make_request(
method="GET",
endpoint=endpoint,
params=params,
response_model=ResponseModelType,
)
```
## Model Generation Rules
When creating new Pydantic models:
```python
from pydantic import BaseModel, Field
from typing import List, Optional
class ResourceResponse(BaseModel):
"""Response model for a single resource.
This model validates and serializes API responses according to
the OpenAPI specification.
"""
id: str = Field(..., description="Unique identifier")
name: str = Field(..., description="Resource name")
optional_field: Optional[str] = Field(None, description="Optional field")
nested_items: List[str] = Field(default_factory=list, description="List of items")
model_config = ConfigDict(str_strip_whitespace=True)
```
## Error Handling Rules
### Exception Usage
- Use `ElytraAuthenticationError` for 401/403 responses
- Use `ElytraNotFoundError` for 404 responses
- Use `ElytraValidationError` for validation failures
- Use `ElytraAPIError` as fallback for other 4xx/5xx errors
### Pattern
```python
try:
response = self.session.request(...)
response.raise_for_status()
if response_model:
return response_model.model_validate(response.json())
return response.json()
except requests.HTTPError as e:
_handle_http_error(e)
raise # Type guard for type checker
except requests.RequestException as e:
raise ElytraAPIError(f"Network error: {str(e)}") from e
except ValidationError as e:
raise ElytraValidationError(
f"Response validation failed: {str(e)}"
) from e
```
## CI/CD Integration
### Pre-Commit Requirements
- All code must be formatte with Black
- All imports must be sorted with isort
- All tests must pass
- Type checking must pass
### Pull Request Checks
- Code coverage must remain ≥80%
- No type checking errors
- No linting errors
- All tests pass
## Dependencies Management
### Current Dependencies
- `requests>=2.28.0` - HTTP client
- `python-dotenv>=0.21.0` - Environment configuration
- `pydantic>=2.0.0` - Data validation
### Dev Dependencies
- `pytest>=7.0.0` - Testing
- `pytest-cov>=4.0.0` - Coverage reporting
- `black>=23.0.0` - Code formatting
- `isort>=5.12.0` - Import sorting
- `flake8>=6.0.0` - Code linting
- `mypy>=1.0.0` - Static type checking
### Rules
- ❌ Do not add dependencies without strong justification
- ❌ Do not update major versions without testing
- ✅ Keep dependency list minimal
- ✅ Pin minor versions for stability
## Documentation Updates
### When to Update
- README.md: When features change
- Docstrings: Always for public APIs
- CHANGELOG.md (if exists): For releases
- Code comments: When logic changes
### When NOT to Update
- Do not update examples that aren't broken
- Do not add optional documentation
- Do not rewrite for style alone
## Troubleshooting for Agents
### Common Issues
**ImportError or ModuleNotFoundError**
- Ensure virtual environment is activated
- Verify dependencies installed: `pip install -e ".[dev]"`
- Check Python path includes project root
**Type Checking Failures**
- Review mypy errors carefully
- Use `Optional[T]` or `T | None` for nullable types
- Use proper return types on all functions
- Check imports from pydantic match v2 API
**Test Failures**
- Always mock external requests
- Verify test isolation (no shared state)
- Check assertions match actual API behavior
- Review mocked response structure matches models
**Formatting Issues**
- Run Black first: `black elytra_client tests`
- Then isort: `isort elytra_client tests`
- Line length is 100 characters
- Follow PEP 8 style guide
## Key Principles
1. **Reliability**: Every change must be tested
2. **Type Safety**: Full type hints, verified with mypy
3. **Consistency**: Follow established patterns
4. **Documentation**: All changes must be documented in English
5. **Simplicity**: Keep modifications focused and atomic
6. **Quality Gates**: Never skip formatting or testing
## Success Criteria for Automated Tasks
A task is complete when:
- ✅ All code passes Black formatting
- ✅ All imports pass isort checks
- ✅ All code passes Flake8 linting
- ✅ All code passes mypy type checking
- ✅ All tests pass
- ✅ Code coverage ≥80%
- ✅ All docstrings are complete and in English
- ✅ No warnings or errors in logs
## Reporting
When completing tasks, report:
1. Changes made (file, function, type of change)
2. Tests added/modified (test names)
3. Coverage impact (before/after percentage)
4. Any issues encountered and resolutions
5. Commands used for verification

284
.copilot-instructions.md Normal file
View file

@ -0,0 +1,284 @@
# GitHub Copilot Instructions for Elytra PIM Client
## Project Overview
**Elytra PIM Client** is a fully Pythonic, Pydantic-driven client for the Elytra Product Information Management (PIM) API. It provides type-safe, validated interactions with the Elytra PIM API using modern Python practices.
### Key Characteristics
- Python 3.9+ project with full type hints
- Pydantic v2 for data validation and serialization
- OpenAPI specification-driven API design
- Comprehensive error handling with custom exception classes
- Bearer token authentication
- Context manager support
- Fully tested with pytest
## Code Style and Conventions
### Language
- **All code and documentation must be in English**.
- No comments, docstrings, variable names, or documentation in other languages.
- Use clear, professional American English.
### Formatting and Standards
- **Code Style**: Follow PEP 8 with line length of 100 characters
- **Formatter**: Black (configured in project)
- **Import Order**: Use isort with Black-compatible settings
- **Linting**: Flake8 for code quality checks
- **Type Checking**: mypy for static type checking
### Type Hints
- **Always include type hints** for function parameters and return types
- Use `Optional[T]` or `T | None` for nullable types
- Use `TypeVar` for generic functions
- Use union types with `|` operator (Python 3.10+)
- For APIs returning multiple response types, use `TypeVar` bound to `BaseModel`
### Docstrings
- Use Google-style docstrings for all public modules, classes, and functions
- Format:
```python
"""One-line summary.
Longer description if needed.
Args:
param_name: Description of parameter
Returns:
Description of return value
Raises:
ExceptionType: When this exception is raised
"""
```
### Imports
- Group imports: stdlib, third-party, local
- Use absolute imports
- Keep imports at module level unless lazy loading is necessary
## Project Structure
```
elytra_client/
├── __init__.py # Package exports
├── client.py # Main ElytraClient class
├── config.py # Configuration handling
├── models.py # Pydantic models (auto-generated from OpenAPI)
└── exceptions.py # Custom exception classes
tests/
├── test_client.py # Client tests
└── ... # Other test files
examples/
└── ... # Example usage scripts
openapi.yaml # OpenAPI specification (source of truth)
pyproject.toml # Project metadata and dependencies
```
## Core Patterns and Conventions
### Exception Handling
- Use custom exception classes from `exceptions.py`:
- `ElytraAPIError`: Base exception for all API errors
- `ElytraAuthenticationError`: Authentication failures (401, 403)
- `ElytraNotFoundError`: Resource not found (404)
- `ElytraValidationError`: Validation failures
- Always catch `requests.RequestException` in HTTP methods
- Always catch `ValidationError` when working with Pydantic models
- Provide meaningful error messages with context
### API Methods in ElytraClient
- Follow REST conventions: `get_*`, `create_*`, `update_*`, `delete_*`
- Return Pydantic models for single resources or dict with `items` list for collections
- Support pagination parameters: `page`, `limit`, `offset`
- Support language parameter: `lang` (for multi-language support)
- Use `_make_request()` helper for all HTTP calls
- Always validate responses with Pydantic models
### Data Models (Pydantic)
- All models inherit from `pydantic.BaseModel`
- Use `Field()` for validation and documentation
- Use `field_validator()` for custom validation
- Model names follow pattern: `Single{Resource}{Operation}RequestBody`, `{Resource}Response`, etc.
- Use `exclude_none=True` when serializing to avoid sending null values
### Configuration
- Use environment variables via `python-dotenv`
- Store sensitive data (API keys) in `.env` files
- Never commit `.env` files
- Provide `.env.example` as template
### Logging
- Use Python's `logging` module
- Create loggers with `logger = logging.getLogger(__name__)`
- Log important operations and errors
- Avoid logging sensitive information (API keys, tokens)
## Testing Requirements
### Test Structure
- All tests in `tests/` directory
- Test file naming: `test_*.py`
- Test class naming: `Test*`
- Test function naming: `test_*`
### Coverage
- Aim for >80% code coverage
- Test both success and error paths
- Test Pydantic validation
- Use pytest fixtures for setup
### Mocking
- Use `unittest.mock` for mocking requests
- Mock external API calls
- Mock database calls if applicable
- Do not make real API calls in tests
### Example Test Pattern
```python
import pytest
from unittest.mock import Mock, patch
def test_get_products(client):
"""Test successful product retrieval."""
with patch.object(client.session, 'request') as mock_request:
# Setup mock
mock_request.return_value = Mock(status_code=200, json=lambda: {...})
# Execute
result = client.get_products()
# Assert
assert isinstance(result, dict)
```
## Development Workflow
### Before Committing
- Run tests: `pytest tests/ -v`
- Check coverage: `pytest --cov=elytra_client`
- Format code: `black elytra_client tests`
- Check imports: `isort elytra_client tests`
- Lint code: `flake8 elytra_client tests`
- Type check: `mypy elytra_client`
### Creating New Features
1. Create a test first (TDD approach if possible)
2. Implement the feature
3. Ensure all tests pass
4. Run formatters and linters
5. Add documentation for public APIs
### API Changes
- Update `openapi.yaml` first (source of truth)
- Regenerate/update Pydantic models in `models.py`
- Update client methods accordingly
- Update tests
## Common Tasks and Expectations
### When Adding API Methods
1. Add method to `ElytraClient` class
2. Create corresponding Pydantic models for request/response
3. Use `_make_request()` helper
4. Add comprehensive docstrings
5. Include type hints
6. Error handling with custom exceptions
7. Add tests with mocked responses
### When Adding Pydantic Models
- Place in `models.py`
- Use descriptive names matching API structure
- Add field validators if needed
- Include docstrings for complex fields
- Use `ConfigDict` for model configuration if needed
### When Fixing Bugs
1. Add a failing test that reproduces the bug
2. Fix the bug
3. Verify the test now passes
4. Check for similar issues in codebase
### When Refactoring
1. Ensure all tests pass before starting
2. Make small, focused changes
3. Run tests frequently
4. Update documentation if interfaces change
5. Commit atomic, logical changes
## Performance Considerations
- Reuse `requests.Session` for connection pooling
- Support pagination for large result sets
- Document timeout behavior
- Consider request/response sizes
- Log performance-critical operations
## Documentation Requirements
### README
- Keep up-to-date with current features
- Include installation and quick start
- Link to full API documentation
### Code Comments
- Explain "why" not "what" - code should be self-explanatory for "what"
- Avoid obvious comments
- Use for complex algorithms or non-obvious decisions
- Keep comments synchronized with code
### Docstrings
- Always provide docstrings for public APIs
- Include examples for complex usage patterns
- Multi-language support should be documented
## What NOT to Do
- ❌ Don't mix English with other languages in code
- ❌ Don't commit `.env` files or secrets
- ❌ Don't make real API calls in tests
- ❌ Don't skip type hints
- ❌ Don't ignore linting or formatting errors
- ❌ Don't commit code that doesn't pass tests
- ❌ Don't use bare `except:` clauses
- ❌ Don't modify response models without updating tests
- ❌ Don't log sensitive information
- ❌ Don't hardcode API keys or credentials
## Helpful Commands
```bash
# Install development dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run with coverage
pytest --cov=elytra_client --cov-report=html
# Format code
black elytra_client tests
isort elytra_client tests
# Lint
flake8 elytra_client tests
# Type check
mypy elytra_client
# Run all checks (recommended before commit)
black elytra_client tests && isort elytra_client tests && flake8 elytra_client tests && mypy elytra_client && pytest tests/ -v
```
## Resources
- **Pydantic v2 Docs**: https://docs.pydantic.dev/latest/
- **OpenAPI Specification**: See `openapi.yaml` in project root
- **Type Hints**: https://docs.python.org/3/library/typing.html
- **Pytest Documentation**: https://docs.pytest.org/
- **PEP 8 Style Guide**: https://www.python.org/dev/peps/pep-0008/

617
DEVELOPMENT.md Normal file
View 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

590
STYLE_GUIDE.md Normal file
View file

@ -0,0 +1,590 @@
# Code Style Guide - Quick Reference
## Language: ENGLISH ONLY
All code, comments, docstrings, and variable names **MUST** be in English.
---
## Formatting
### Line Length
- **Maximum: 100 characters**
- Configure editor ruler: `View → Toggle Rulers`
### Indentation
- **4 spaces** (no tabs)
- Auto-enforced by Black
### Blank Lines
- **2 blank lines** between module-level definitions
- **1 blank line** between class methods
- **1 blank line** inside functions for logical grouping
---
## Imports
### Order
```python
# 1. Standard library
import sys
import os
from pathlib import Path
from typing import Optional, Dict, List, TypeVar, Type
# 2. Third-party
import requests
from pydantic import BaseModel, Field, ValidationError
from dotenv import load_dotenv
# 3. Local/relative
from .exceptions import ElytraAPIError
from .models import ProductResponse
```
### Rules
- ✅ Use absolute imports
- ✅ Group by: stdlib → third-party → local
- ✅ Alphabetical within groups
- ❌ No `from module import *` (use explicit imports)
- ❌ No unused imports (checked by flake8)
---
## Type Hints
### Functions
```python
# Good
def get_product(
product_id: str,
lang: str = "en"
) -> ProductResponse:
"""Get a product."""
pass
# Good - with Optional
def find_product(
name: Optional[str] = None
) -> Optional[ProductResponse]:
"""Find product by name."""
pass
# Good - with Union (Python 3.10+)
def process(data: dict | list) -> str:
"""Process data."""
pass
# Bad - missing type hints
def get_products(product_id):
return response
```
### Variables
```python
# At module level
API_TIMEOUT: int = 30
DEFAULT_LANG: str = "en"
ALLOWED_LANGS: List[str] = ["en", "de", "fr"]
# In functions
active_products: List[Product] = []
product_map: Dict[str, Product] = {}
result: Optional[ProductResponse] = None
```
### Generic Types
```python
from typing import TypeVar, Generic, List
T = TypeVar('T', bound=BaseModel)
def validate_response(
data: dict,
model: Type[T]
) -> T:
"""Generic response validator."""
return model.model_validate(data)
```
---
## Docstrings (Google Style)
### Module Level
```python
"""Module description.
Longer explanation if needed.
This module handles product management operations including
creating, updating, and retrieving products from the API.
"""
```
### Classes
```python
class ElytraClient:
"""A Pythonic client for the Elytra PIM API.
This client provides convenient methods for interacting with
the Elytra PIM API, including authentication and product management.
Args:
base_url: The base URL of the Elytra PIM API
api_key: The API key for authentication
timeout: Request timeout in seconds (default: 30)
Attributes:
session: Requests.Session instance for connection pooling
"""
pass
```
### Functions/Methods
```python
def create_product(
self,
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.
Args:
name: Product name (required, max 255 chars)
description: Product description
lang: Language code (default: "en")
Returns:
SingleProductResponse: The newly created product with ID
Raises:
ElytraValidationError: If validation fails
ElytraAuthenticationError: If authentication fails
ElytraAPIError: For other API errors
Example:
>>> client = ElytraClient(base_url="...", api_key="...")
>>> product = client.create_product("Widget", "A useful widget")
>>> print(product.id)
"""
pass
```
---
## Naming Conventions
### Constants
```python
# UPPER_CASE_WITH_UNDERSCORES
API_TIMEOUT = 30
MAX_RETRIES = 3
DEFAULT_LANGUAGE = "en"
```
### Classes
```python
# PascalCase
class ElytraClient:
pass
class ProductResponse:
pass
class InvalidProductError:
pass
```
### Functions & Methods
```python
# snake_case
def get_products():
pass
def create_new_product():
pass
def validate_input():
pass
```
### Variables
```python
# snake_case
product_id = "123"
api_key = "secret"
response_data = {}
is_valid = True
```
### Private Methods/Attributes
```python
# Leading underscore for private
def _make_request(self):
pass
def _validate_response(self, response):
pass
_cache = {}
_internal_state = None
```
### Constants vs Variables
```python
# Constants (module-level, unchanging)
DEFAULT_TIMEOUT = 30
API_VERSION = "v1"
# Variables (local, changeable)
timeout = config.get("timeout", DEFAULT_TIMEOUT)
request_id = str(uuid4())
```
---
## Strings
### Formatting
```python
# f-strings (Python 3.6+) - PREFERRED
name = "Product"
print(f"Created product: {name}")
# Format method
print("Created product: {}".format(name))
# % formatting - AVOID
print("Created product: %s" % name)
```
### Quotes
```python
# Use double quotes as default
message = "This is a message"
docstring = """Multi-line docstring used with three
double quotes."""
# Use single quotes when string contains double quote
message = 'He said "Hello"'
```
---
## Exceptions
### Handling
```python
# Good - specific exception, with context
try:
response = self.session.request(method, url)
response.raise_for_status()
except requests.HTTPError as e:
if e.response.status_code == 404:
raise ElytraNotFoundError("Resource not found") from e
raise ElytraAPIError(f"HTTP Error: {e}") from e
except requests.RequestException as e:
raise ElytraAPIError(f"Network error: {str(e)}") from e
# Bad - too broad, no context
try:
response = requests.get(url)
except:
print("Error")
# Bad - bare raise without context
except Exception:
return None
```
### Raising
```python
# Good - with meaningful message
raise ElytraValidationError(f"Invalid product ID: {product_id}")
# Good - with cause chain
try:
data = pydantic_model.model_validate(response)
except ValidationError as e:
raise ElytraValidationError(f"Response validation failed") from e
# Bad - generic message
raise ElytraAPIError("Error")
```
---
## Pydantic Models
```python
from pydantic import BaseModel, Field, ConfigDict
class ProductResponse(BaseModel):
"""Response model for a single product.
Validates and represents a product object from the API.
"""
# Required field with description
id: str = Field(..., description="Unique product identifier")
name: str = Field(..., description="Product name", min_length=1, max_length=255)
# Optional field with default
description: Optional[str] = Field(None, description="Product description")
# Field with default value
language: str = Field("en", description="Language code")
# List field with default factory
tags: List[str] = Field(default_factory=list, description="Product tags")
# Configuration
model_config = ConfigDict(
str_strip_whitespace=True, # Automatically strip whitespace from strings
validate_default=True, # Validate default values
)
```
---
## Comments
### Good Comments
```python
# Good - explains WHY, not WHAT
# We need to retry failed requests as the API has rate limiting
for retry in range(MAX_RETRIES):
try:
return self._make_request(...)
except RequestException:
if retry == MAX_RETRIES - 1:
raise
# Good - documents non-obvious code
# Using session instead of individual requests for connection pooling
self.session = requests.Session()
# Good - TODO comments
# TODO: implement pagination for large datasets (issue #123)
def get_all_products(self):
pass
```
### Bad Comments
```python
# Bad - obvious, doesn't add value
# Increment counter
counter += 1
# Bad - outdated/wrong info
# This will be removed next release (said in 2020)
def deprecated_method(self):
pass
# BAD - NEVER DO THIS
# código de prueba
# 这个是测试代码
# Ceci est un test
```
---
## Control Flow
### If Statements
```python
# Good - clear and readable
if user_is_authenticated and not is_rate_limited:
return process_request(data)
# Bad - too complex
if x and y or z and not w:
pass
# Good - use `in` for membership
if method in ("GET", "POST", "PUT"):
pass
# Good - use `is` for None, True, False
if result is None:
pass
if is_valid is True: # or just: if is_valid:
pass
```
### Loops
```python
# Good - enumerate for index and value
for idx, item in enumerate(items):
print(f"{idx}: {item}")
# Good - dict iteration
for key, value in config.items():
print(f"{key} = {value}")
# Avoid - unnecessary else clause
for item in items:
if match(item):
break
else:
# Only executes if loop completed without break
# Often confusing - better to use explicit flag
handle_not_found()
```
---
## Context Managers
```python
# Good - automatic cleanup
with session.get(url) as response:
data = response.json()
# For custom classes
class ElytraClient:
def __enter__(self):
"""Enter context."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context and cleanup."""
self.session.close()
return False # Don't suppress exceptions
# Usage
with ElytraClient(base_url, api_key) as client:
products = client.get_products()
```
---
## Whitespace and Blank Lines
### Module Structure
```python
"""Module docstring."""
# Imports (with blank line after)
import os
from typing import List
# Module constants (with blank line before and after)
DEFAULT_TIMEOUT = 30
API_VERSION = "v1"
# Classes (with 2 blank lines before)
class FirstClass:
"""Class docstring."""
def method_one(self):
"""First method."""
pass
def method_two(self):
"""Second method."""
pass
class SecondClass:
"""Another class."""
pass
# Module-level functions (with 2 blank lines before)
def module_function():
"""A module-level function."""
pass
```
---
## Line Breaking
```python
# Good - break long imports
from pydantic import (
BaseModel,
Field,
ValidationError,
ConfigDict,
)
# Good - break long function signatures
def create_product(
self,
name: str,
description: Optional[str] = None,
lang: str = "en",
) -> SingleProductResponse:
pass
# Good - break long lines with backslash (function calls)
result = self._make_request(
method="POST",
endpoint="/products",
json_data=product_data,
response_model=ProductResponse,
)
# Good - break long conditionals
if (
user_is_authenticated and
not is_rate_limited and
has_valid_request_data
):
process_request()
```
---
## Linting and Formatting Commands
```bash
# Format with Black
black elytra_client tests
# Sort imports with isort
isort elytra_client tests
# Lint with Flake8
flake8 elytra_client tests
# Type check with mypy
mypy elytra_client
# Run all (recommended before commit)
black elytra_client tests && \
isort elytra_client tests && \
flake8 elytra_client tests && \
mypy elytra_client && \
pytest tests/ -v
```
---
## Summary Checklist
Before committing code:
- [ ] All code is in **English**
- [ ] **No lines exceed 100 characters** (Black will enforce)
- [ ] **Type hints** on all functions
- [ ] **Docstrings** on all public APIs (Google style)
- [ ] **No unused imports** (Flake8 checks)
- [ ] **Proper exception handling** with meaningful messages
- [ ] **Tests** for new code
- [ ] Run Black and isort
- [ ] Run Flake8
- [ ] Run mypy
- [ ] Run pytest