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
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
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 fields (V1 API).

        For list/person/company field metadata, prefer the V2 read endpoints on the
        corresponding services when available (e.g., `client.lists.get_fields(...)`).
        """
        params: dict[str, Any] = {}
        if list_id:
            params["list_id"] = int(list_id)
        if entity_type is not None:
            params["entity_type"] = int(entity_type)

        data = self._client.get("/fields", params=params or None, v1=True)
        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
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
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
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
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
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
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 fields (V1 API).

For list/person/company field metadata, prefer the V2 read endpoints on the corresponding services when available (e.g., client.lists.get_fields(...)).

Source code in affinity/services/v1_only.py
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def list(
    self,
    *,
    list_id: ListId | None = None,
    entity_type: EntityType | None = None,
) -> list[FieldMetadata]:
    """
    Get fields (V1 API).

    For list/person/company field metadata, prefer the V2 read endpoints on the
    corresponding services when available (e.g., `client.lists.get_fields(...)`).
    """
    params: dict[str, Any] = {}
    if list_id:
        params["list_id"] = int(list_id)
    if entity_type is not None:
        params["entity_type"] = int(entity_type)

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