# Story 1.2: Generated Client & Data Models

Status: done

## Story

As a Developer,
I want to generate Pydantic models from the Allure TestOps OpenAPI spec,
so that I can interact with the API with 100% schema fidelity and strict type safety.

## Acceptance Criteria

1. **Given** the Allure TestOps OpenAPI 3.1 spec, **when** I run the client generator, **then** a full Python client is successfully created in `src/client/generated/`.
2. A thin `httpx`-based `AllureClient` is created in `src/client/client.py` wrapping the generated client.
3. `mypy --strict` passes for the generated client.

## Tasks / Subtasks

- [x] **Task 1: Obtain Allure TestOps OpenAPI Spec** (AC: #1)
  - [x] 1.1: Document the location of the OpenAPI spec (from Allure TestOps instance at `/api/v2/api-docs` or Swagger UI)
  - [x] 1.2: Create `openapi/allure-testops-service/` directory for storing the OpenAPI spec
  - [x] 1.3: Download/Save the OpenAPI spec as `openapi/allure-testops-service/report-service.json`
  - [x] 1.4: Validate the spec file is valid OpenAPI 3.x format (use `datamodel-code-generator` validation)

- [x] **Task 2: Install and Configure openapi-generator-cli** (AC: #1)
  - [x] 2.1: Add `openapi-generator-cli` as dev dependency via `uv add --dev openapi-generator-cli`
  - [x] 2.2: Create `scripts/openapi-generator-config.yaml` with Python/Httpx settings
  - [x] 2.3: Use `scripts/filter_openapi.py` to pre-filter the OpenAPI spec
  - [x] 2.4: Create `scripts/generate_testops_api_client.sh` for reproducible client generation
  - [x] 2.5: Create `.openapi-generator-ignore` to protect manual `__init__.py` files

- [x] **Task 3: Generate Python Client** (AC: #1, #3)
  - [x] 3.1: Run generator: `./scripts/generate_testops_api_client.sh`
  - [x] 3.2: Verify the filtered spec contains only essential controllers
  - [x] 3.3: Verify `api_client.py`, `models`, and `api` packages are generated

- [x] **Task 4: Implement AllureClient Wrapper** (AC: #2, #3)
  - [x] 4.1: Create `src/client/__init__.py` with public exports
  - [x] 4.2: Create `src/client/client.py` with async `AllureClient` class
  - [x] 4.3: Implement `__init__` with base_url and token parameters (using `SecretStr`)
  - [x] 4.4: Implement async context manager (`__aenter__`, `__aexit__`) for httpx lifecycle
  - [x] 4.5: Implement `_request` helper method for all HTTP operations with error handling
  - [x] 4.6: Add placeholder methods matching MVP API operations (see Client Method Stubs below)
  - [x] 4.7: Run `mypy --strict src/client/client.py` and fix any type errors
  - [x] 4.1: Create `src/client/__init__.py` for public exports (manually maintained)
  - [x] 4.2: Implement `src/client/client.py` wrapping the generated `TestCaseControllerApi` and `TestCaseAttachmentControllerApi`
  - [x] 4.3: Ensure `TestCaseOverviewDto` and other key models are correctly imported
  - [x] 4.4: Run `mypy --strict` on manual client files

- [x] **Task 5: Define Custom Exceptions** (AC: #2)
  - [x] 5.1: Create `src/client/exceptions.py` with `AllureAPIError` base exception
  - [x] 5.2: Add specific exceptions: `AllureNotFoundError`, `AllureValidationError`, `AllureAuthError`, `AllureRateLimitError`
  - [x] 5.3: Include response body and status code in exception details for debugging

- [x] **Task 6: Add Integration Points** (AC: #2)
  - [x] 6.1: Update `src/client/__init__.py` to export `AllureClient`, key models, and exceptions
  - [x] 6.2: Create `tests/unit/test_client.py` with basic client instantiation tests
  - [x] 6.3: Create `tests/unit/test_models.py` to validate key models parse correctly

- [x] **Task 7: Quality Assurance** (AC: #3)
  - [x] 7.1: Run `ruff check src/client/` and fix any linting issues  
  - [x] 7.2: Run `mypy --strict src/client/` ensuring zero type errors
  - [x] 7.3: Run `pytest tests/unit/test_client.py tests/unit/test_models.py --alluredir=allure-results`
  - [x] 7.4: Verify model generation is documented in README for future regeneration

- [x] **Task 8: E2E Test Preparation** (AC: implicit, NFR11)
  - [x] 8.1: Create `tests/e2e/test_client_connectivity.py` for sandbox connection test
  - [x] 8.2: Write E2E test verifying AllureClient can authenticate with sandbox
  - [x] 8.3: Configure allure-pytest reporting for E2E tests

## Dev Notes

### Architecture Compliance (CRITICAL)

**Generated Code Policy:**
- `src/client/generated` is AUTO-GENERATED - **DO NOT manually edit these files**
- Use `.openapi-generator-ignore` to protect top-level `__init__.py` files if they contain manual logic.
- The client uses the `httpx` library to match the project's primary HTTP client and allow easy mocking in tests.

**Secret Handling:**
- Use `pydantic.SecretStr` for API tokens in client configuration
- Client MUST NOT log raw token values

### openapi-generator-cli Configuration

**Installation:**
```bash
uv add --dev openapi-generator-cli
```

**Generation Command:**
See `scripts/generate_testops_api_client.sh`

### Selective Generation
To keep the client minimal, we use a 2-step process:
1. `scripts/filter_openapi.py`: Manually filters `openapi/allure-testops-service/report-service.json` to keep only required controllers, outputting to `filtered-report-service.json`.
2. `scripts/generate_testops_api_client.sh`: Generates the client from the filtered spec using `scripts/openapi-generator-config.yaml`.

### Protection of Manual Files
The `.openapi-generator-ignore` file protects manual files and is used by the generation script.

### AllureClient Implementation Pattern

**Required Pattern (async context manager):**
```python
from typing import Any
from pydantic import SecretStr
import httpx

class AllureClient:
    """Async client for Allure TestOps API.
    
    Usage:
        async with AllureClient(base_url, token) as client:
            cases = await client.list_test_cases(project_id=123)
    """
    
    def __init__(
        self,
        base_url: str,
        token: SecretStr,
        timeout: float = 30.0,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._token = token
        self._timeout = timeout
        self._client: httpx.AsyncClient | None = None
    
    async def __aenter__(self) -> "AllureClient":
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={"Authorization": f"Bearer {self._token.get_secret_value()}"},
            timeout=self._timeout,
        )
        return self
    
    async def __aexit__(self, *args: Any) -> None:
        if self._client:
            await self._client.aclose()
```

### Client Method Stubs (MVP Operations)

The client should have placeholder methods for all MVP operations. Implementation comes in later stories:

```python
# Test Case operations (Story 1.3, 1.4, 1.5)
async def create_test_case(self, project_id: int, data: TestCaseCreate) -> TestCase: ...
async def get_test_case(self, test_case_id: int) -> TestCase: ...
async def update_test_case(self, test_case_id: int, data: TestCaseUpdate) -> TestCase: ...
async def delete_test_case(self, test_case_id: int) -> None: ...

# Shared Step operations (Story 2.1, 2.2, 2.3)
async def create_shared_step(self, project_id: int, data: SharedStepCreate) -> SharedStep: ...
async def list_shared_steps(self, project_id: int) -> list[SharedStep]: ...
async def update_shared_step(self, step_id: int, data: SharedStepUpdate) -> SharedStep: ...
async def delete_shared_step(self, step_id: int) -> None: ...

# Search operations (Story 3.1, 3.2, 3.3)
async def list_test_cases(self, project_id: int, **filters: Any) -> list[TestCaseSummary]: ...
async def search_test_cases(self, project_id: int, query: str) -> list[TestCaseSummary]: ...
```

### Obtaining the OpenAPI Spec

**Option 1 (Recommended):** From Allure TestOps Swagger UI
```
https://<your-allure-instance>/api/v2/api-docs
```

**Option 2:** Export from Swagger UI
1. Navigate to `https://<your-allure-instance>/swagger-ui.html`
2. Click "Download" or copy the JSON spec

**Note:** The developer implementing this story will need access to an Allure TestOps instance or a provided OpenAPI spec file.

### Key Models to Validate

After generation, verify these critical models exist and have correct structure:
- `TestCase` / `TestCaseCreate` / `TestCaseUpdate`
- `TestStep` / `TestStepCreate`
- `SharedStep` / `SharedStepCreate`
- `Tag`
- `CustomField` / `CustomFieldValue`
- `Attachment`

### Exception Hierarchy

```python
# src/client/exceptions.py
class AllureAPIError(Exception):
    """Base exception for all Allure API errors."""
    def __init__(self, message: str, status_code: int | None = None, response_body: str | None = None):
        super().__init__(message)
        self.status_code = status_code
        self.response_body = response_body

class AllureNotFoundError(AllureAPIError):
    """Resource not found (404)."""
    pass

class AllureValidationError(AllureAPIError):
    """Validation failed (400)."""
    pass

class AllureAuthError(AllureAPIError):
    """Authentication failed (401/403)."""
    pass

class AllureRateLimitError(AllureAPIError):
    """Rate limit exceeded (429)."""
    pass
```

### Previous Story Intelligence

**From Story 1.1:**
- Project structure already created with `src/client/` directory
- `SecretStr` masking logic implemented in `src/utils/logger.py`
- `AllureAPIError` base exception defined in `src/utils/error.py` - **coordinate with this**
- Global exception handler converts errors to "Agent Hints"

**Coordination Required:**
- The `AllureAPIError` in `src/client/exceptions.py` should either:
  - (A) Import and extend from `src/utils/error.AllureAPIError`, OR
  - (B) Be the primary exception that `src/utils/error.py` catches
- Recommend option (B): Client defines the exception, utils catches it

### Project Structure After This Story

```
lucius-mcp/
├── .openapi-generator-ignore          # Protection rules
├── openapi/
│   └── allure-testops-service/
│       ├── report-service.json        # Full OpenAPI spec
│       └── filtered-report-service.json # Filtered OpenAPI spec
├── scripts/
│   ├── filter_openapi.py              # Filtering logic
│   ├── generate_testops_api_client.sh # Reproducible generation script
│   └── openapi-generator-config.yaml  # Generation settings
├── src/
│   ├── client/
│   │   ├── __init__.py                # Manual public exports
│   │   ├── client.py                  # AllureClient wrapper
│   │   └── generated/                 # Generated minimal client
└── tests/
    └── unit/
        └── test_client.py             # Verified with unit tests
```

### References

- [Source: specs/architecture.md#Data Architecture]
- [Source: specs/architecture.md#API & Communication Patterns]
- [Source: specs/project-context.md#Pydantic & Data]
- [Source: specs/prd.md#Data Schemas & Formatting]
- [Source: Story 1.1 - Project Structure]
- [External: datamodel-code-generator v0.49.0 documentation]

## Dev Agent Record

### Agent Model Used

Antigravity (Claude-based)

### Completion Notes List

- ✅ Optimized `openapi-generator-cli` process to generate a minimal client.
- ✅ Reduced model count from 2500+ to ~685 by filtering via `openapi-normalizer`.
- ✅ Switched library to `httpx` to align with project dependencies and fix test mocking (using `respx`).
- ✅ Protected manual initialization files from being overwritten using `.openapi-generator-ignore`.
- ✅ Verified with 13 unit tests (`tests/unit/test_client.py`) passing successfully.
- ✅ Cleaned up legacy `datamodel-code-generator` approach and full client folder.
- ✅ **[Review Fix]** Restored missing E2E connectivity tests.
- ✅ **[Review Fix]** Eliminated legacy directories (`generated_full`, `models`) for cleaner codebase.
- ✅ **[Review Fix]** Synchronized story documentation with actual implementation.

### File List

**New Files:**
- .openapi-generator-ignore
- scripts/filter_openapi.py
- scripts/openapi-generator-config.yaml
- scripts/generate_testops_api_client.sh
- src/client/generated/
- src/client/exceptions.py

**Modified Files:**
- src/client/__init__.py (Restored and protected)
- src/client/client.py (Updated to use optimized imports)
- tests/unit/test_client.py (Passing with httpx-based generated client)
- specs/implementation-artifacts/1-2-generated-client-and-data-models.md (This document)
