Source code for fca_api.types.base

"""Base classes for FCA API type definitions.

This module provides the foundation classes for all FCA API response models.
These base classes handle common patterns like:

- **Field normalization**: Converting API field names to lowercase
- **Extra field handling**: Capturing unexpected fields for compatibility
- **Validation configuration**: Consistent validation behavior across types
- **Skip markers**: Ignoring fields marked as "[notinuse]" by the API

The base classes ensure consistent behavior across all API response types
while providing flexibility for handling API changes and variations.

Classes:
    - `Base`: Standard base class for most API types
    - `RelaxedBase`: Base class that captures extra/unknown fields

Example:
    Creating a custom API type::

        class MyFirmData(Base):
            name: str
            frn: str
            status: str

        # Validates and normalizes field names
        data = MyFirmData.model_validate({
            "Name": "Test Firm",     # -> "name"
            "FRN": "123456",        # -> "frn"
            "Status": "Active"      # -> "status"
        })

    Using RelaxedBase for unknown fields::

        class FlexibleType(RelaxedBase):
            required_field: str

        data = FlexibleType.model_validate({
            "required_field": "value",
            "unknown_field": "captured"
        })

        print(data.get_additional_fields())  # {"unknown_field": "captured"}
"""

import typing

import pydantic

from . import settings


[docs] class Base(pydantic.BaseModel): """Base class for FCA API response models. This base class provides common functionality for all FCA API types: - **Field name normalization**: Converts API field names to lowercase - **Skip unused fields**: Ignores fields marked with "[notinuse]" - **Consistent validation**: Uses package-wide validation settings - **Type safety**: Provides proper Pydantic model inheritance All API response models should inherit from this class to ensure consistent behavior and proper handling of API response variations. Features: - Automatic lowercase conversion of field names - Filtering of "[notinuse]" fields from API responses - Standardized validation configuration - Support for both strict and flexible validation modes Example: Define a custom API model:: class FirmSummary(Base): name: str frn: str status: str # API response with mixed case field names api_data = { "Name": "Example Firm", "FRN": "123456", "Status": "Authorised", "Internal_Field[notinuse]": "ignored" } # Validation handles normalization automatically firm = FirmSummary.model_validate(api_data) print(firm.name) # "Example Firm" print(firm.frn) # "123456" print(firm.status) # "Authorised" # "Internal_Field[notinuse]" is automatically ignored Note: The `model_validate` method is customized to handle FCA API response patterns. Use this instead of the standard Pydantic constructor when working with raw API data. """
[docs] @classmethod def model_validate(cls, data: typing.Any) -> "Base": """Validate and create model instance from API response data. This method extends Pydantic's validation to handle FCA API response patterns including field name normalization and filtering of unused fields. Args: data: Raw data from API response, typically a dictionary with mixed-case field names and potential "[notinuse]" markers. Returns: Validated model instance with normalized field names. Example: Validate API response data:: api_response = { "FirmName": "Test Corp", "FRN": "123456", "Old_Field[notinuse]": "ignored" } firm = FirmModel.model_validate(api_response) # Field names normalized, unused fields filtered """ if isinstance(data, dict): updated_data = {} for key, value in data.items(): if isinstance(key, str): key = key.lower().strip() if "[notinuse]" in key: # Skip fields that are marked as not in use continue updated_data[key] = value data = updated_data if cls.model_config and cls.model_config.get("extra"): # Use model-specific extra settings if defined return super().model_validate(data) else: return super().model_validate(data, extra=settings.model_validate_extra)
[docs] class RelaxedBase(Base): """Base class for API types that capture additional/unknown fields. This class extends the standard `Base` class to capture and store any fields that are not explicitly defined in the model schema. This is useful for: - **Forward compatibility**: Capturing new API fields before updating schemas - **Debugging**: Seeing what unexpected data the API returns - **Flexible processing**: Accessing both known and unknown fields - **API evolution**: Handling API changes gracefully The extra fields are stored in Pydantic's `__pydantic_extra__` and can be accessed via the `get_additional_fields()` method. Example: Handle API responses with unknown fields:: class FlexibleFirmData(RelaxedBase): name: str frn: str # API returns extra fields not in schema api_data = { "name": "Test Firm", "frn": "123456", "new_field_v2": "future_data", "experimental_flag": True } firm = FlexibleFirmData.model_validate(api_data) print(firm.name) # "Test Firm" print(firm.frn) # "123456" # Access additional fields extra = firm.get_additional_fields() print(extra) # {"new_field_v2": "future_data", "experimental_flag": True} Use Cases: - Capturing new API fields during development - Building flexible data processing pipelines - Debugging unexpected API responses - Future-proofing against API changes Warning: While flexible, this approach can hide schema drift and API changes. Use primarily for development and debugging, not production parsing. """ model_config = pydantic.ConfigDict(extra="allow")
[docs] def get_additional_fields(self) -> dict[str, typing.Any]: """Get all additional fields not defined in the model schema. Returns: Dictionary containing all extra fields captured during validation that were not part of the model's defined fields. Example: Access unexpected API fields:: firm = FlexibleFirmData.model_validate(api_response) # Check for new/unknown fields extra_fields = firm.get_additional_fields() if extra_fields: print(f"API returned unexpected fields: {list(extra_fields.keys())}") # Log for analysis logger.info("Unknown API fields", extra=extra_fields) """ return dict(self.__pydantic_extra__)