juno.contacts

Read and write the on-device contacts database.

Wraps CNContactStore for full-access read (get_all, search, get) and write (add, delete). Authorization is requested lazily by the read/write helpers; the prompt is presented once per fresh install and the resolved status is reused thereafter. Stop is responsive while the iOS prompt is on screen — pressing Stop raises KeyboardInterrupt.

Typical usage:

from juno import contacts

# `is_authorized()` returns True for both AUTHORIZED (full)
# and LIMITED (iOS 18+ user-allowlisted subset); reads /
# writes work under either grant.
if not contacts.is_authorized():
    contacts.request_authorization()
if not contacts.is_authorized():
    raise SystemExit("contacts permission needed")

for c in contacts.search("alex"):
    print(c.given_name, c.family_name)
    for phone in c.phone_numbers:
        print(" ", phone.label, phone.value)

new = contacts.add(contacts.Contact(
    given_name="Ada",
    family_name="Lovelace",
    phone_numbers=(contacts.PhoneNumber(label="work", value="+44 20 7123 4567"),),
))
contacts.delete(new.identifier)

Limitations:

  • Under iOS 18+ limited authorization the app sees only the contacts the user explicitly allowlisted in the system picker. is_authorized() returns True (limited is a working state), but get_all() / search() will surface only the allowlisted subset, and the system silently drops delete / update calls that target non-allowlisted contacts.

  • Contact.note is not exposed: in iOS 13+ Apple gates the notes field behind the com.apple.developer.contacts.notes entitlement, which Juno does not request.

class juno.contacts.AuthorizationStatus

Bases: object

String constants returned by authorization_status().

class juno.contacts.PhoneNumber(label='', value='')

Bases: object

A single labeled phone number on a Contact.

label is a readable string like "home", "work", "mobile", "iPhone", "main" (the framework’s known labels round-trip; user-defined labels pass through verbatim). value is the raw phone string as the user entered it.

Parameters:
class juno.contacts.EmailAddress(label='', value='')

Bases: object

A single labeled email address on a Contact.

Parameters:
class juno.contacts.PostalAddress(label='', street='', sub_locality='', city='', sub_administrative_area='', state='', postal_code='', country='', iso_country_code='')

Bases: object

A single structured postal address on a Contact.

All sub-fields are strings. Empty values mean “not set” — the Contacts framework silently elides them on save.

Parameters:
  • label (str)

  • street (str)

  • sub_locality (str)

  • city (str)

  • sub_administrative_area (str)

  • state (str)

  • postal_code (str)

  • country (str)

  • iso_country_code (str)

class juno.contacts.Contact(identifier='', given_name='', family_name='', middle_name='', organization_name='', phone_numbers=<factory>, email_addresses=<factory>, postal_addresses=<factory>, birthday=None)

Bases: object

A single contact record.

All fields default to empty strings / empty tuples so a partially populated contact can be passed to add() without ceremony. The identifier is assigned by iOS when the contact is saved; it is empty for newly-constructed (not-yet-added) contacts.

The labeled-value collections accept any iterable on construction and are normalized to tuple so the dataclass remains hashable and immutable.

Parameters:
juno.contacts.authorization_status()

Return the current contacts authorization status.

Returns:

One of the AuthorizationStatus string constants.

Return type:

str

juno.contacts.is_authorized()

Return True if the app has any usable contacts authorization.

Both "authorized" (full) and "limited" (iOS 18+ user-allowlisted subset) return True — read and write paths succeed under either grant; limited simply restricts the visible set to whatever the user picked. Callers that need to distinguish full vs. limited can branch on authorization_status() directly.

Return type:

bool

juno.contacts.request_authorization()

Request contacts authorization if status is undetermined.

If the status is already determined, returns immediately with the existing value. Otherwise this triggers the system permission prompt and waits until the user makes a choice. Pressing Juno’s Stop button while the prompt is on screen raises a KeyboardInterrupt.

Returns:

Resolved authorization status, one of the AuthorizationStatus constants.

Return type:

str

juno.contacts.get_all()

Return every readable contact in the user’s database.

Raises:
  • PermissionError – If contacts authorization is denied or restricted. (limited counts as authorized — see module docs.)

  • RuntimeError – If the contact store could not be enumerated.

Return type:

list[Contact]

juno.contacts.search(name)

Find contacts whose unified name matches name.

The match is case- and diacritic-insensitive prefix matching, the same predicate CNContact.predicateForContacts(matchingName:) provides.

Parameters:

name (str) – Partial or full name to search for.

Returns:

A list of matching Contact records, possibly empty.

Raises:
  • TypeError – If name is not a string.

  • PermissionError – If contacts authorization is denied or restricted. (limited counts as authorized — see module docs.)

  • RuntimeError – If the contact store query failed.

Return type:

list[Contact]

juno.contacts.get(identifier)

Fetch a single contact by its system identifier.

Parameters:

identifier (str) – The opaque identifier returned by an earlier get_all() / search() / add().

Returns:

The Contact, or None if no contact with that identifier exists.

Raises:
  • TypeError – If identifier is not a string.

  • PermissionError – If contacts authorization is denied or restricted. (limited counts as authorized — see module docs.)

Return type:

Contact | None

juno.contacts.add(contact)

Persist a new contact to the user’s database.

Parameters:

contact (Contact) – The Contact to add. identifier is ignored on input — the system assigns a new one.

Returns:

A fresh Contact carrying the assigned identifier and the saved fields.

Raises:
  • TypeError – If contact is not a Contact, or any of its fields have the wrong runtime type (the dataclass type hints are advisory — they don’t enforce str / date at construction).

  • PermissionError – If contacts authorization is denied or restricted. (limited counts as authorized — see module docs.)

  • RuntimeError – If the save failed (e.g. validation rejection by the framework).

Return type:

Contact

juno.contacts.delete(contact)

Delete a contact by Contact or identifier string.

Parameters:

contact (Contact | str) – A previously-fetched Contact, or its identifier string directly.

Returns:

True if the contact was deleted, False if no contact with that identifier exists or the delete failed.

Raises:
  • TypeError – If contact is not a Contact or a string.

  • ValueError – If contact is a Contact whose identifier is empty (i.e. a not-yet-saved contact).

  • PermissionError – If contacts authorization is denied or restricted. (limited counts as authorized — see module docs.)

Return type:

bool