From 459838b2e68b9f2fa07fa4b901e779b041748a51 Mon Sep 17 00:00:00 2001 From: claudi Date: Fri, 20 Feb 2026 09:14:40 +0100 Subject: [PATCH] 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. --- .agent-instructions.md | 303 +++++++++++++++++++ .copilot-instructions.md | 284 ++++++++++++++++++ DEVELOPMENT.md | 617 +++++++++++++++++++++++++++++++++++++++ STYLE_GUIDE.md | 590 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1794 insertions(+) create mode 100644 .agent-instructions.md create mode 100644 .copilot-instructions.md create mode 100644 DEVELOPMENT.md create mode 100644 STYLE_GUIDE.md diff --git a/.agent-instructions.md b/.agent-instructions.md new file mode 100644 index 0000000..4a53031 --- /dev/null +++ b/.agent-instructions.md @@ -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___` + +### 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 diff --git a/.copilot-instructions.md b/.copilot-instructions.md new file mode 100644 index 0000000..1bf22d4 --- /dev/null +++ b/.copilot-instructions.md @@ -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/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..0fb25e2 --- /dev/null +++ b/DEVELOPMENT.md @@ -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 diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000..4127cf6 --- /dev/null +++ b/STYLE_GUIDE.md @@ -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