Skip to content

Fields (V1)

Service for managing custom fields.

Use V2 /fields endpoints for reading field metadata. Use V1 for creating/deleting fields.

Source code in affinity/services/v1_only.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
class FieldService:
    """
    Service for managing custom fields.

    Use V2 /fields endpoints for reading field metadata.
    Use V1 for creating/deleting fields.
    """

    def __init__(self, client: HTTPClient):
        self._client = client

    def list(
        self,
        *,
        list_id: ListId | None = None,
        entity_type: EntityType | None = None,
    ) -> list[FieldMetadata]:
        """
        Get field metadata.

        Results are cached for 5 minutes when caching is enabled on the client.

        Args:
            list_id: Filter to fields for a specific list
            entity_type: Filter to fields for a specific entity type

        Returns:
            List of field metadata
        """
        params: dict[str, Any] = {}
        if list_id is not None:
            params["list_id"] = int(list_id)
        if entity_type is not None:
            params["entity_type"] = int(entity_type)

        list_key = "all" if list_id is None else int(list_id)
        type_key = "all" if entity_type is None else int(entity_type)
        cache_key = f"field:v1_list_{list_key}:type_{type_key}"

        data = self._client.get(
            "/fields",
            params=params or None,
            v1=True,
            cache_key=cache_key,
            cache_ttl=300,
        )
        items = data.get("data", [])
        if not isinstance(items, list):
            items = []
        return [FieldMetadata.model_validate(f) for f in items]

    def create(self, data: FieldCreate) -> FieldMetadata:
        """Create a custom field."""
        value_type_code = to_v1_value_type_code(value_type=data.value_type, raw=None)
        if value_type_code is None:
            raise ValueError(f"Field value_type has no V1 numeric mapping: {data.value_type!s}")
        payload = data.model_dump(by_alias=True, mode="json", exclude_unset=True, exclude_none=True)
        payload["entity_type"] = int(data.entity_type)
        payload["value_type"] = value_type_code
        for key in ("allows_multiple", "is_list_specific", "is_required"):
            if not payload.get(key):
                payload.pop(key, None)

        result = self._client.post("/fields", json=payload, v1=True)

        # Invalidate field caches
        if self._client.cache:
            self._client.cache.invalidate_prefix("field")
            self._client.cache.invalidate_prefix("list_")
            self._client.cache.invalidate_prefix("person_fields")
            self._client.cache.invalidate_prefix("company_fields")

        return FieldMetadata.model_validate(result)

    def delete(self, field_id: FieldId) -> bool:
        """
        Delete a custom field (V1 API).

        Note: V1 deletes require numeric field IDs. The SDK accepts V2-style
        `field-<digits>` IDs and converts them; enriched/relationship-intelligence
        IDs are not supported.
        """
        numeric_id = field_id_to_v1_numeric(field_id)
        result = self._client.delete(f"/fields/{numeric_id}", v1=True)

        # Invalidate field caches
        if self._client.cache:
            self._client.cache.invalidate_prefix("field")
            self._client.cache.invalidate_prefix("list_")
            self._client.cache.invalidate_prefix("person_fields")
            self._client.cache.invalidate_prefix("company_fields")

        return bool(result.get("success", False))

    def exists(self, field_id: AnyFieldId) -> bool:
        """
        Check if a field exists.

        Useful for validation before setting field values.

        Note: This fetches all fields and checks locally. If your code calls
        exists() frequently in a loop, consider caching the result of fields.list()
        yourself.

        Args:
            field_id: The field ID to check

        Returns:
            True if the field exists, False otherwise

        Example:
            if client.fields.exists(FieldId("field-123")):
                client.field_values.create(...)
        """
        target_id = FieldId(field_id) if not isinstance(field_id, FieldId) else field_id
        fields = self.list()
        return any(f.id == target_id for f in fields)

    def get_by_name(self, name: str) -> FieldMetadata | None:
        """
        Find a field by its display name.

        Uses case-insensitive matching (casefold for i18n support).

        Note: This fetches all fields and searches locally. If your code calls
        get_by_name() frequently in a loop, consider caching the result of
        fields.list() yourself.

        Args:
            name: The field display name to search for

        Returns:
            FieldMetadata if found, None otherwise

        Example:
            field = client.fields.get_by_name("Primary Email Status")
            if field:
                fv = client.field_values.get_for_entity(field.id, person_id=pid)
        """
        fields = self.list()
        name_folded = name.strip().casefold()  # Strip whitespace, then casefold for i18n
        for field in fields:
            if field.name.casefold() == name_folded:
                return field
        return None

create(data: FieldCreate) -> FieldMetadata

Create a custom field.

Source code in affinity/services/v1_only.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
def create(self, data: FieldCreate) -> FieldMetadata:
    """Create a custom field."""
    value_type_code = to_v1_value_type_code(value_type=data.value_type, raw=None)
    if value_type_code is None:
        raise ValueError(f"Field value_type has no V1 numeric mapping: {data.value_type!s}")
    payload = data.model_dump(by_alias=True, mode="json", exclude_unset=True, exclude_none=True)
    payload["entity_type"] = int(data.entity_type)
    payload["value_type"] = value_type_code
    for key in ("allows_multiple", "is_list_specific", "is_required"):
        if not payload.get(key):
            payload.pop(key, None)

    result = self._client.post("/fields", json=payload, v1=True)

    # Invalidate field caches
    if self._client.cache:
        self._client.cache.invalidate_prefix("field")
        self._client.cache.invalidate_prefix("list_")
        self._client.cache.invalidate_prefix("person_fields")
        self._client.cache.invalidate_prefix("company_fields")

    return FieldMetadata.model_validate(result)

delete(field_id: FieldId) -> bool

Delete a custom field (V1 API).

Note: V1 deletes require numeric field IDs. The SDK accepts V2-style field-<digits> IDs and converts them; enriched/relationship-intelligence IDs are not supported.

Source code in affinity/services/v1_only.py
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
def delete(self, field_id: FieldId) -> bool:
    """
    Delete a custom field (V1 API).

    Note: V1 deletes require numeric field IDs. The SDK accepts V2-style
    `field-<digits>` IDs and converts them; enriched/relationship-intelligence
    IDs are not supported.
    """
    numeric_id = field_id_to_v1_numeric(field_id)
    result = self._client.delete(f"/fields/{numeric_id}", v1=True)

    # Invalidate field caches
    if self._client.cache:
        self._client.cache.invalidate_prefix("field")
        self._client.cache.invalidate_prefix("list_")
        self._client.cache.invalidate_prefix("person_fields")
        self._client.cache.invalidate_prefix("company_fields")

    return bool(result.get("success", False))

exists(field_id: AnyFieldId) -> bool

Check if a field exists.

Useful for validation before setting field values.

Note: This fetches all fields and checks locally. If your code calls exists() frequently in a loop, consider caching the result of fields.list() yourself.

Parameters:

Name Type Description Default
field_id AnyFieldId

The field ID to check

required

Returns:

Type Description
bool

True if the field exists, False otherwise

Example

if client.fields.exists(FieldId("field-123")): client.field_values.create(...)

Source code in affinity/services/v1_only.py
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
def exists(self, field_id: AnyFieldId) -> bool:
    """
    Check if a field exists.

    Useful for validation before setting field values.

    Note: This fetches all fields and checks locally. If your code calls
    exists() frequently in a loop, consider caching the result of fields.list()
    yourself.

    Args:
        field_id: The field ID to check

    Returns:
        True if the field exists, False otherwise

    Example:
        if client.fields.exists(FieldId("field-123")):
            client.field_values.create(...)
    """
    target_id = FieldId(field_id) if not isinstance(field_id, FieldId) else field_id
    fields = self.list()
    return any(f.id == target_id for f in fields)

get_by_name(name: str) -> FieldMetadata | None

Find a field by its display name.

Uses case-insensitive matching (casefold for i18n support).

Note: This fetches all fields and searches locally. If your code calls get_by_name() frequently in a loop, consider caching the result of fields.list() yourself.

Parameters:

Name Type Description Default
name str

The field display name to search for

required

Returns:

Type Description
FieldMetadata | None

FieldMetadata if found, None otherwise

Example

field = client.fields.get_by_name("Primary Email Status") if field: fv = client.field_values.get_for_entity(field.id, person_id=pid)

Source code in affinity/services/v1_only.py
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
def get_by_name(self, name: str) -> FieldMetadata | None:
    """
    Find a field by its display name.

    Uses case-insensitive matching (casefold for i18n support).

    Note: This fetches all fields and searches locally. If your code calls
    get_by_name() frequently in a loop, consider caching the result of
    fields.list() yourself.

    Args:
        name: The field display name to search for

    Returns:
        FieldMetadata if found, None otherwise

    Example:
        field = client.fields.get_by_name("Primary Email Status")
        if field:
            fv = client.field_values.get_for_entity(field.id, person_id=pid)
    """
    fields = self.list()
    name_folded = name.strip().casefold()  # Strip whitespace, then casefold for i18n
    for field in fields:
        if field.name.casefold() == name_folded:
            return field
    return None

list(*, list_id: ListId | None = None, entity_type: EntityType | None = None) -> list[FieldMetadata]

Get field metadata.

Results are cached for 5 minutes when caching is enabled on the client.

Parameters:

Name Type Description Default
list_id ListId | None

Filter to fields for a specific list

None
entity_type EntityType | None

Filter to fields for a specific entity type

None

Returns:

Type Description
list[FieldMetadata]

List of field metadata

Source code in affinity/services/v1_only.py
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
def list(
    self,
    *,
    list_id: ListId | None = None,
    entity_type: EntityType | None = None,
) -> list[FieldMetadata]:
    """
    Get field metadata.

    Results are cached for 5 minutes when caching is enabled on the client.

    Args:
        list_id: Filter to fields for a specific list
        entity_type: Filter to fields for a specific entity type

    Returns:
        List of field metadata
    """
    params: dict[str, Any] = {}
    if list_id is not None:
        params["list_id"] = int(list_id)
    if entity_type is not None:
        params["entity_type"] = int(entity_type)

    list_key = "all" if list_id is None else int(list_id)
    type_key = "all" if entity_type is None else int(entity_type)
    cache_key = f"field:v1_list_{list_key}:type_{type_key}"

    data = self._client.get(
        "/fields",
        params=params or None,
        v1=True,
        cache_key=cache_key,
        cache_ttl=300,
    )
    items = data.get("data", [])
    if not isinstance(items, list):
        items = []
    return [FieldMetadata.model_validate(f) for f in items]