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+
limitedauthorization the app sees only the contacts the user explicitly allowlisted in the system picker.is_authorized()returnsTrue(limited is a working state), butget_all()/search()will surface only the allowlisted subset, and the system silently drops delete / update calls that target non-allowlisted contacts.Contact.noteis not exposed: in iOS 13+ Apple gates the notes field behind thecom.apple.developer.contacts.notesentitlement, which Juno does not request.
- class juno.contacts.AuthorizationStatus¶
Bases:
objectString constants returned by
authorization_status().
- class juno.contacts.PhoneNumber(label='', value='')¶
Bases:
objectA single labeled phone number on a
Contact.labelis a readable string like"home","work","mobile","iPhone","main"(the framework’s known labels round-trip; user-defined labels pass through verbatim).valueis the raw phone string as the user entered it.
- class juno.contacts.EmailAddress(label='', value='')¶
Bases:
objectA single labeled email address on a
Contact.
- class juno.contacts.PostalAddress(label='', street='', sub_locality='', city='', sub_administrative_area='', state='', postal_code='', country='', iso_country_code='')¶
Bases:
objectA 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.
- 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:
objectA single contact record.
All fields default to empty strings / empty tuples so a partially populated contact can be passed to
add()without ceremony. Theidentifieris 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
tupleso the dataclass remains hashable and immutable.- Parameters:
identifier (str)
given_name (str)
family_name (str)
middle_name (str)
organization_name (str)
phone_numbers (tuple[PhoneNumber, ...])
email_addresses (tuple[EmailAddress, ...])
postal_addresses (tuple[PostalAddress, ...])
birthday (date | None)
- juno.contacts.authorization_status()¶
Return the current contacts authorization status.
- Returns:
One of the
AuthorizationStatusstring constants.- Return type:
- juno.contacts.is_authorized()¶
Return
Trueif the app has any usable contacts authorization.Both
"authorized"(full) and"limited"(iOS 18+ user-allowlisted subset) returnTrue— read and write paths succeed under either grant;limitedsimply restricts the visible set to whatever the user picked. Callers that need to distinguish full vs. limited can branch onauthorization_status()directly.- Return type:
- 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
AuthorizationStatusconstants.- Return type:
- 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:
- 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
Contactrecords, possibly empty.- Raises:
TypeError – If
nameis 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:
- 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, orNoneif no contact with that identifier exists.- Raises:
TypeError – If
identifieris 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
Contactto add.identifieris ignored on input — the system assigns a new one.- Returns:
A fresh
Contactcarrying the assigned identifier and the saved fields.- Raises:
TypeError – If
contactis not aContact, or any of its fields have the wrong runtime type (the dataclass type hints are advisory — they don’t enforcestr/dateat 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:
- juno.contacts.delete(contact)¶
Delete a contact by
Contactor identifier string.- Parameters:
contact (Contact | str) – A previously-fetched
Contact, or its identifier string directly.- Returns:
Trueif the contact was deleted,Falseif no contact with that identifier exists or the delete failed.- Raises:
ValueError – If
contactis aContactwhoseidentifieris 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: