Reading and writing¶
Your buildings, units, contacts and call log are read-only through the API. Access codes can also be created, changed and deleted.
Reading¶
get() returns one object by its UUID, and raises NotFoundError if it isn't within your API key's reach. list() returns an iterator of every object. It requests nothing until you start iterating, then fetches more as it goes. Stop iterating, and it stops requesting.
A list shows changes made while you iterate it: deleting or changing the items you're listing can make it skip some, so finish the list before changing what's in it.
from yama import Yama
client = Yama()
units = list(client.units.list())
for unit in units:
print(unit.name, unit.door_digit)
building = client.buildings.get(units[0].building)
Filters¶
Filters are keyword arguments with the API's names. Leave one out to not filter by it. UUIDs can be UUID objects or strings, and enum filters take the enum or its string value.
from yama import DoorDigit
for unit in client.units.list(building=building.uuid, door_digit=DoorDigit.NINE):
print(unit.name)
cleaners = list(client.codes.list(search="clean")) # names containing "clean", any case
A UUID filter that isn't a valid UUID raises ValueError without sending a request. A unit or building filter your API key can't reach gives an empty list, not an error.
What you get back¶
Every response is a frozen Pydantic model with the API's field names: UUIDs are UUID, and every time is a timezone-aware datetime in UTC.
code = next(client.codes.list())
print(code.uuid, code.unit, code.code, code.name, code.expires_at)
print(code.model_dump(mode="json")) # a dict, ready for JSON
The API adds fields and enum values over time without a new version, and the client is ready for them:
- A field the client doesn't know yet is still there: read it as an attribute, or from
code.model_extra. - An enum value the client doesn't know yet comes back as a plain string. Compare with
==, which works for both:call.result == CallResult.REJECTED. Enum members format as their value, sostr()and f-strings give"rejected"either way.
The call log¶
calls.list() takes an optional start and end, as timezone-aware datetimes. It yields the calls that started at or after start and before end, newest first. A call at exactly end is left out, so consecutive ranges never miss or repeat a call. A local day runs from its midnight to the next midnight, in any time zone:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from yama import CallResult
vancouver = ZoneInfo("America/Vancouver")
day = datetime(2026, 10, 10, tzinfo=vancouver)
for call in client.calls.list(start=day, end=day + timedelta(days=1)):
print(call.started_at.astimezone(vancouver), call.result, call.summary)
rejected = list(client.calls.list(start=day, result=CallResult.REJECTED))
- A
date, or a datetime without a time zone, raises before any request is sent. - Leave out
startorendto leave that side open; with neither, you get all history. - With a
start, polling for recent calls is cheap. - Each call comes once, even when new calls arrive while you iterate.
If you keep your own copy of the call log, it changes after the fact:
- Calls rebuilt from the phone carrier's records, with
importedset, can appear up to 48 hours after they started (longer after an API outage), listed bystarted_at, not by when they appeared. - A call whose
resultisin_progressgets its final result when it ends. - A call's other details can change too, so match calls by
uuid.
Creating¶
codes.create() takes an AccessCodeCreate. Request models check fields, types and the values the schema allows as you build them: code is 3 to 15 digits, and name at most 255 characters (see What a call raises). Rules that depend on the API's data, such as a code being unique on its unit or expires_at being in the future, are checked by the API, which answers with BadRequestError.
from datetime import datetime, timezone
from yama import AccessCodeCreate
unit = next(client.units.list())
expires = datetime(2027, 1, 1, tzinfo=timezone.utc)
code = client.codes.create(
AccessCodeCreate(unit=unit.uuid, code="4321", name="Cleaner", expires_at=expires)
)
A create is safe to repeat with the same idempotency key; see Creates that are safe to repeat.
Changing¶
codes.update() takes an AccessCodeUpdate and changes only the fields you set. Setting name or expires_at to None clears it; code and unit can't be cleared.
from yama import AccessCodeUpdate
code = client.codes.update(code.uuid, AccessCodeUpdate(name="Cleaner (Mon-Fri)"))
code = client.codes.update(code.uuid, AccessCodeUpdate(expires_at=None)) # never expires
codes.replace() takes a whole AccessCodeCreate; leaving name or expires_at unset keeps its current value, and None clears it. To start from a code you fetched, copy only the fields the request model has, so this keeps working when the API adds fields to its responses:
fields = code.model_dump(include=set(AccessCodeCreate.model_fields))
code = client.codes.replace(code.uuid, AccessCodeCreate(**(fields | {"code": "8642"})))
Deleting¶
codes.delete() deletes a code, and it stops working at once:
client.codes.delete(code.uuid)