Skip to content

Refactor async metadata.py to reduce code duplication with helper methods #116

@btoron

Description

@btoron

Refactor async metadata.py - Reduce Code Duplication

Overview

Analysis of ofsc/async_client/metadata.py (2,340 lines) identified 6 repetitive patterns that can be refactored using helper methods and generics.

Estimated Reduction: ~533 lines (~23% of file)


Patterns Identified

Pattern Occurrences Current Lines After Refactor Lines Saved
GET List with Pagination 16 ~224 ~36 ~188
GET Single Item by Label 17 ~238 ~37 ~201
GET All (no pagination) 4 ~48 ~19 ~29
PUT/Create-Replace 4 ~72 ~37 ~35
DELETE 2 ~24 ~14 ~10
Links Removal 40+ ~80 ~10 ~70
Total - ~686 ~153 ~533

Pattern 1: GET List with Pagination (16 methods)

Affected Methods:

  • get_activity_type_groups, get_activity_types, get_forms
  • get_inventory_types, get_languages, get_link_templates
  • get_map_layers, get_non_working_reasons, get_properties
  • get_routing_profiles, get_shifts, get_time_slots
  • get_work_skills, get_work_skill_groups, get_work_zones
  • get_capacity_categories

Refactor: Create _get_paginated_list() helper with TypeVar generic


Pattern 2: GET Single Item by Label (17 methods)

Affected Methods:

  • get_activity_type_group, get_activity_type, get_application
  • get_capacity_area, get_capacity_category, get_form
  • get_inventory_type, get_language, get_link_template
  • get_map_layer, get_non_working_reason, get_organization
  • get_property, get_shift, get_time_slot
  • get_work_skill, get_workzone

Refactor: Create _get_single_item() helper with TypeVar generic


Pattern 3: GET All (4 methods)

Affected Methods:

  • get_applications, get_organizations
  • get_resource_types, get_capacity_areas

Refactor: Create _get_all_items() helper


Pattern 4: PUT/Create-Replace (4 methods)

Affected Methods:

  • create_or_replace_property, create_workzone
  • replace_workzone, create_or_update_enumeration_values

Refactor: Create _put_item() helper


Pattern 5: DELETE (2 methods)

Affected Methods:

  • delete_workzone, delete_property

Refactor: Create _delete_item() helper


Pattern 6: Links Removal (40+ methods)

Inconsistent patterns for removing 'links' from API responses

Refactor: Create _clean_response() helper used by all helpers


Bug Found: Inconsistent quote_plus Usage

Some methods encode labels with quote_plus(), others don't:

Method Uses quote_plus
get_activity_type ✅ Yes
get_property No
get_workzone No
get_form ✅ Yes

Risk: Labels with special characters (spaces, +, %) may fail

Fix: Standardize by encoding all labels in _get_single_item() helper


Proposed Helper Methods

from typing import TypeVar, Type
T = TypeVar("T")

def _clean_response(self, data: dict) -> dict:
    """Remove API-specific keys not in models."""
    data.pop("links", None)
    return data

async def _get_paginated_list(
    self,
    endpoint: str,
    response_model: Type[T],
    error_context: str,
    offset: int = 0,
    limit: int = 100,
    extra_params: dict | None = None
) -> T:
    """Generic GET list with pagination."""
    ...

async def _get_single_item(
    self,
    endpoint_template: str,
    label: str,
    response_model: Type[T],
    error_context: str
) -> T:
    """Generic GET single item by label."""
    ...

async def _get_all_items(
    self,
    endpoint: str,
    response_model: Type[T],
    error_context: str
) -> T:
    """Generic GET all items without pagination."""
    ...

async def _put_item(
    self,
    endpoint: str,
    data: BaseModel,
    response_model: Type[T],
    error_context: str
) -> T:
    """Generic PUT item."""
    ...

async def _delete_item(
    self,
    endpoint_template: str,
    label: str,
    error_context: str
) -> None:
    """Generic DELETE item by label."""
    ...

Example: Before and After

Before (14 lines):

async def get_activity_types(self, offset: int = 0, limit: int = 100) -> ActivityTypeListResponse:
    """Get activity types with pagination."""
    url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypes")
    params = {"offset": offset, "limit": limit}
    try:
        response = await self._client.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        data = response.json()
        if "links" in data and not hasattr(ActivityTypeListResponse, "links"):
            del data["links"]
        return ActivityTypeListResponse.model_validate(data)
    except httpx.HTTPStatusError as e:
        self._handle_http_error(e, "Failed to get activity types")
        raise
    except httpx.TransportError as e:
        raise OFSCNetworkError(f"Network error: {str(e)}") from e

After (7 lines):

async def get_activity_types(self, offset: int = 0, limit: int = 100) -> ActivityTypeListResponse:
    """Get activity types with pagination."""
    return await self._get_paginated_list(
        "/rest/ofscMetadata/v1/activityTypes",
        ActivityTypeListResponse,
        "Failed to get activity types",
        offset, limit
    )

Implementation Plan

Phase 1: Add Helper Methods

  1. Add _clean_response() helper
  2. Add _get_paginated_list() helper
  3. Add _get_single_item() helper
  4. Add _get_all_items() helper
  5. Add _put_item() helper
  6. Add _delete_item() helper

Phase 2: Refactor Methods

  1. Refactor 16 paginated list methods
  2. Refactor 17 single item methods
  3. Refactor 4 get-all methods
  4. Refactor 4 PUT methods
  5. Refactor 2 DELETE methods

Phase 3: Bug Fixes

  1. Fix quote_plus in get_property, get_workzone
  2. Standardize links removal pattern

Testing Strategy

# Run tests before each phase
uv run pytest tests/async/ -v

# Ensure 100% test pass rate after each change

Benefits

  1. Reduced LOC: ~533 lines removed (~23%)
  2. Better Maintainability: Single source of truth for API patterns
  3. Consistency: All methods follow same error handling pattern
  4. Bug Fixes: Standardizes quote_plus usage
  5. Type Safety: TypeVar generics enable IDE type inference
  6. Easier Testing: Helpers can be tested independently

References

  • Analysis Plan: See /Users/borja.toron/.claude/plans/magical-bouncing-curry.md
  • Target File: ofsc/async_client/metadata.py

Metadata

Metadata

Assignees

No one assigned

    Labels

    asyncAsync client implementationenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions