# Task Recon: Implement CSV reading for locations and policies with graceful degradation

**Task ID:** 5.2
**Status:** pending
**Generated:** 2026-01-28T21:15:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Add methods to read locations and policies CSV files from the cache directory using the stdlib csv module. Return empty lists when CSV files don't exist to support graceful degradation.

### Parent Task Context

**Parent Task:** Implement CacheManager (ID: 5)
**Parent Goal:** Create the CacheManager for token caching, CSV file reading, and response caching with TTL support.
**This Subtask's Role:** This subtask extends the CacheManager class (created in 5.1) with CSV file reading capabilities. The CSV data provides emergency locations and policy definitions that are exported via PowerShell since no Graph API endpoints exist for these resources.

### Dependencies
- Depends on: 5.1 (Implement token storage and retrieval with JSON persistence) - **DONE**

### Acceptance Criteria
1. `read_locations_csv() -> list[dict]` reads `{cache_dir}/locations.csv` using `csv.DictReader`
2. `read_policies_csv() -> list[dict]` reads `{cache_dir}/policies.csv` using `csv.DictReader`
3. Internal `_read_csv(filename: str) -> list[dict]` helper to avoid code duplication
4. Return empty list if file doesn't exist (graceful degradation - no ValidationError)
5. Handle encoding as UTF-8 with BOM support (PowerShell exports may include BOM)
6. Catch `csv.Error` and return empty list (with optional warning log)

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 5.1 | Token storage and retrieval | CacheManager class exists with `_cache_dir` Path property; use same file I/O patterns |

### Reusable Patterns from Dependencies

**From Subtask 5.1 Implementation Notes:**
- **Path expansion pattern**: `Path(str_or_none or DEFAULT).expanduser()` - already used in `cache_manager.py:36-40`
- **Silent failure for missing files**: `get_token()` returns None for missing files - CSV methods should return `[]` similarly
- **Properties for directories**: `cache_dir` property exposes `_cache_dir` for external read access

**File I/O pattern from cache_manager.py:102:**
```python
content = token_path.read_text(encoding="utf-8")
```

### Warnings and Gotchas
- **UTF-8 BOM from PowerShell**: PowerShell `Export-Csv -Encoding UTF8` on Windows includes a BOM (Byte Order Mark). Use `encoding="utf-8-sig"` to handle this automatically.
- **extra="forbid" on Pydantic models**: Not relevant here since we return raw dicts, not Pydantic models
- **No logging infrastructure yet**: The codebase has no logging setup. For MVP, return empty list silently; logging can be added later.

### Open Items Inherited
- [ ] (From 5.1) Subtask 5.2: Implement CSV file reading methods (`read_locations_csv`, `read_policies_csv`) - **THIS TASK**
- [ ] (From 5.1) Subtask 5.3: Implement staleness detection (`get_csv_metadata`, `get_cache_age_days`, `is_cache_stale`)

---

## Architecture Context

### Relevant Design Decisions
- **CacheManager is lowest-level infrastructure** - has no dependencies on other components (see architecture doc 5-components.md:130-145)
- **CSV files are reference data** - exported via PowerShell, read-only from Python CLI perspective
- **Graceful degradation** - return empty list when files don't exist (per CLI spec architecture-hybrid-graph-api-csv-export.md:35-37)

### Module Structure
```
src/teams_phone/
├── infrastructure/
│   ├── __init__.py           # Already exports CacheManager
│   └── cache_manager.py      # MODIFY - add CSV methods
├── constants.py              # DEFAULT_CACHE_PATH (already imported)
└── exceptions.py             # Not needed - no exceptions for missing CSV
```

### Conventions to Follow
- Use stdlib `csv` module (no external dependencies)
- Docstrings follow Google style (see existing methods in cache_manager.py)
- Type hints required for all functions (mypy strict mode)
- Return `list[dict[str, str]]` since csv.DictReader yields `dict[str, str]` rows
- UTF-8-sig encoding for BOM handling

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/cache_manager.py` | 42-50 | `cache_dir` property - use this to build CSV file paths |
| `src/teams_phone/infrastructure/cache_manager.py` | 96-106 | `get_token()` pattern - graceful None return for missing/invalid files |
| `.taskmaster/docs/graph-api-contracts/7-powershell-csv-export-mvp-solution.md` | 118-138 | CSV schema for locations.csv |
| `.taskmaster/docs/graph-api-contracts/7-powershell-csv-export-mvp-solution.md` | 140-171 | CSV schema for policies.csv |
| `scripts/Export-TeamsPhoneData.ps1` | 228-231 | How locations.csv is exported with UTF8 encoding |
| `scripts/Export-TeamsPhoneData.ps1` | 301-302 | How policies.csv is exported with UTF8 encoding |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `.taskmaster/docs/cli-specification/architecture-hybrid-graph-api-csv-export.md` | 34-38 | Degraded mode behavior when CSV files are missing |
| `tests/unit/test_cache_manager.py` | 1-333 | Existing test patterns for CacheManager |
| `.taskmaster/tasks/task_005.md` | 44-81 | Implementation notes from 5.1 with patterns discovered |

### Code Patterns to Follow

```python
# Graceful file read pattern (from cache_manager.py:96-106)
def get_token(self, tenant_name: str) -> CachedToken | None:
    token_path = self._get_token_path(tenant_name)

    if not token_path.exists():
        return None  # <-- Return None/empty, don't raise

    try:
        content = token_path.read_text(encoding="utf-8")
        return CachedToken.model_validate_json(content)
    except (PydanticValidationError, ValueError):
        return None  # <-- Catch errors and return None/empty
```

```python
# CSV reading pattern (Python stdlib)
import csv

def _read_csv(self, filename: str) -> list[dict[str, str]]:
    """Read a CSV file from the cache directory.

    Args:
        filename: Name of the CSV file (e.g., "locations.csv").

    Returns:
        List of dictionaries, one per row. Empty list if file
        doesn't exist or is malformed.
    """
    csv_path = self._cache_dir / filename

    if not csv_path.exists():
        return []

    try:
        with csv_path.open(encoding="utf-8-sig") as f:
            reader = csv.DictReader(f)
            return list(reader)
    except csv.Error:
        return []
```

---

## Dependencies

### Internal Modules
- `pathlib.Path` - already imported in cache_manager.py
- No new internal imports needed

### External Packages
- `csv` - Python stdlib, no installation needed

### New Dependencies Needed
- None required - stdlib `csv` module is sufficient

---

## Testing Guidance

### Test File Location
`tests/unit/test_cache_manager.py` (EXISTING FILE - add new test classes)

### Test Patterns in Use
```python
# From tests/unit/test_cache_manager.py:88-95
class TestGetToken:
    @pytest.mark.unit
    def test_returns_none_for_missing_file(self, tmp_path: Path) -> None:
        """get_token returns None when token file doesn't exist."""
        manager = CacheManager(token_dir=str(tmp_path))

        result = manager.get_token("nonexistent")

        assert result is None
```

### Fixtures Available
- `tmp_path` - pytest built-in fixture for temporary directories

### Test Cases to Implement
1. `test_read_locations_csv_valid_file` - Valid CSV returns list of dicts with correct keys
2. `test_read_policies_csv_valid_file` - Valid CSV returns list of dicts with correct keys
3. `test_read_csv_missing_file_returns_empty_list` - Missing file returns `[]`
4. `test_read_csv_empty_file_returns_empty_list` - Empty file returns `[]`
5. `test_read_csv_handles_utf8_bom` - File with BOM header is read correctly
6. `test_read_csv_malformed_returns_empty_list` - Malformed CSV returns `[]`
7. `test_read_csv_header_only_returns_empty_list` - CSV with only header returns `[]`

### Sample CSV Fixtures
```python
# locations.csv content
LOCATIONS_CSV = """locationId,civicAddressId,description,companyName,address,city,stateOrProvince,postalCode,countryOrRegion,isDefault
93cb8a70-b4af-41df-9928-d07607e21776,a1b2c3d4-0000-0000-0000-000000000000,Main Office - Floor 3,Contoso Ltd,123 Main St,Seattle,WA,98101,US,True
f47ac10b-58cc-4372-a567-0e02b2c3d479,a1b2c3d4-0000-0000-0000-000000000000,Branch Office,Contoso Ltd,456 Oak Ave,Portland,OR,97201,US,False"""

# policies.csv content
POLICIES_CSV = """policyType,policyName,policyId,description,isGlobal
TeamsCallingPolicy,Global,Global,Default calling policy,True
TeamsCallingPolicy,AllowCalling,AllowCalling,Allows all calling features,False
TeamsVoicemailPolicy,Global,Global,Default voicemail policy,True"""

# UTF-8 BOM prefix (bytes)
UTF8_BOM = b'\xef\xbb\xbf'
```

---

## Risks and Blockers

### Technical Risks
- [ ] **PowerShell CSV encoding variation**: Windows PowerShell may output UTF-8 with BOM, while PowerShell Core may not. Using `utf-8-sig` encoding handles both cases transparently.
- [ ] **Large CSV files**: For MVP, `list(reader)` loads entire file into memory. This is fine for typical tenant sizes (hundreds of locations/policies). For very large tenants, consider streaming iteration later.

### Dependency Risks
- None - Subtask 5.1 is complete, providing the CacheManager class to extend.

### Open Questions
- [ ] Should malformed CSV rows be skipped individually, or should entire file be rejected? Task details say "return empty list with optional warning log" - implement empty list return for MVP.
- [ ] Should we validate CSV headers match expected schema? For MVP, no - just return raw dicts. Schema validation can be added to service layer.

---

## Implementation Recommendations

### Suggested Approach
1. Add `import csv` at top of `cache_manager.py`
2. Implement `_read_csv(filename: str) -> list[dict[str, str]]` private helper
3. Implement `read_locations_csv() -> list[dict[str, str]]` calling `_read_csv("locations.csv")`
4. Implement `read_policies_csv() -> list[dict[str, str]]` calling `_read_csv("policies.csv")`
5. Add comprehensive unit tests following existing patterns

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/infrastructure/cache_manager.py` | Add CSV reading methods |
| Modify | `tests/unit/test_cache_manager.py` | Add CSV reading tests |

### Quick Wins
- Start with `_read_csv()` helper - all logic in one place
- Copy the `get_token()` graceful degradation pattern almost exactly
- Test fixtures are straightforward string literals

---

## Cheat Sheet

### Key Imports
```python
import csv  # Add this import
# Existing imports are sufficient
```

### Key Functions to Use
- `csv.DictReader(file_object)` - Parse CSV into dict rows
- `Path.open(encoding="utf-8-sig")` - Open file with BOM handling
- `Path.exists()` - Check if file exists before reading
- `list(reader)` - Convert DictReader iterator to list

### CSV Column Names (from PowerShell export)
**locations.csv:**
- `locationId`, `civicAddressId`, `description`, `companyName`, `address`, `city`, `stateOrProvince`, `postalCode`, `countryOrRegion`, `isDefault`

**policies.csv:**
- `policyType`, `policyName`, `policyId`, `description`, `isGlobal`

### Common Patterns
- **UTF-8 with BOM**: Use `encoding="utf-8-sig"` instead of `encoding="utf-8"`
- **Graceful degradation**: Return `[]` for missing/malformed files, don't raise exceptions
- **csv.Error handling**: Catch this to handle malformed CSV gracefully

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
