Interacting with the API
This is an example of traversing the Equipment API to get the current position of all equipment in an account. It is written in Python for ease of reading, but any language will work.
The API uses Hypertext As The Engine Of State (HATEOAS). In practice, this means that each response has a list of semantic links that are used to navigate to related resources on the API.
Start by importing some dependencies:
from lib.clients import OAuthClient # A client to make requests with. This abstracts the auth logicfrom lib.consts import API_HOME_URL # so we know where to start making requests tofrom pprint import pprint # pretty printerNow we can create a client and fetch the Home resource. This doesn’t contain any content of its own, it’s just a landing page that has links to other resources.
client = OAuthClient()
# helper function to get URLs out of the links block so we can traverse easierdef get_link_url(json: dict, rel: str, title: str | None = None) -> str: for link in json["links"]: if link["rel"] == rel: if title == None or link["title"] == title: return link["href"]
home = client.get_json(API_HOME_URL)pprint(home)Output:
{'links': [{'href': 'https://example.trimble.com', 'rel': 'self'}, {'href': 'https://example.trimble.com/identity', 'rel': 'identity', 'title': 'me'}]}Every resource has a “self” link. This is a reference to itself, and acts as a unique ID for the resource.
The other link has the relation identity and title me. Semantically, this means that following it will lead to “my identity”.
identity_url = get_link_url(home, "identity", "me")
identity = client.get_json(identity_url)pprint(identity)Output:
{'links': [{'href': 'https://example.trimble.com/identity', 'rel': 'self'}, {'href': 'https://example.trimble.com/identity/equipment', 'rel': 'equipments'}]}From here, we can traverse to equipments - a collection of equipment.
Note that this request is being made from the context of an identity.
Semantically, this means that the results will only contain equipment that is available to the current identity.
equipments_url = get_link_url(identity, "equipments")
equipment_list = client.get_json(equipments_url)pprint(equipment_list)Output:
{'items': [{'id': 'https://example.trimble.com/equipment/fb5a0ba5-4add-4a67-b792-147517c3af08'}, {'id': 'https://example.trimble.com/equipment/08dceda9-83a5-adc3-3593-17648a000104'}, {'id': 'https://example.trimble.com/equipment/a92117ce-283e-4a7d-b9af-d45afd3e0169'}, {'id': 'https://example.trimble.com/equipment/fe2f167d-de4f-4583-bbfe-1743f6ab9bb1'}], 'links': [{'href': 'https://example.trimble.com/identity/equipment', 'rel': 'self'}]}Note that the response has URLs in both links and items.
This is so the client can navigate to items within the collection as well as resources related to it.
We can get a single piece of Equipment as follows:
equipment_urls = [item["id"] for item in equipment_list["items"]]
equipment = client.get_json(equipment_urls[2]) # pick one of the urls as an examplepprint(equipment)Output:
{'links': [{'href': 'https://example.trimble.com/equipment/a92117ce-283e-4a7d-b9af-d45afd3e0169', 'rel': 'self'}, {'href': 'https://example.trimble.com/equipment/a92117ce-283e-4a7d-b9af-d45afd3e0169/position', 'rel': 'position', 'title': 'current'}, {'href': 'https://example.trimble.com/device/a92117ce-283e-4a7d-b9af-d45afd3e0169', 'rel': 'device', 'title': 'primary'}, {'href': 'https://example.trimble.com/equipment/a92117ce-283e-4a7d-b9af-d45afd3e0169/type', 'rel': 'type'}], 'name': 'EC520-P_212'}The equipment resource fans out to a number of other resources. This is for a few reasons:
- Each resource has a different lifecycle. For example, the current position will change more frequently than the name of the equipment, so a client can efficiently poll just the position resource.
- It reduces the response size. This approach may feel like a lot of requests, but they are each very small and can be cached effectively.
In order to get the details for one piece of equipment, simply follow the links to related resources.
def print_equipment_details(url: str): equipment = client.get_json(url) position = client.get_json(get_link_url(equipment, "position", "current")) type_ = client.get_json(get_link_url(equipment, "type")) manufacturer = client.get_json(get_link_url(type_, "manufacturer")) model = client.get_json(get_link_url(type_, "model"))
print(f"A {type_['name']} ({manufacturer['name']} {model['name']}) named \"{equipment['name']}\" is currently at [{position['lat']}, {position['lon']}, {position['h']}]")
for url in equipment_urls: print_equipment_details(url)Output:
A EC520 (EC520 EC520) named "EC520-P_218" is currently at [14.846253138273706, 74.2928582911711, 55755904.99]A Tablet (Caterpillar DG07) named "CB450-P_CB450" is currently at [46.40764494895242, -62.30234170032556, 123.43499755859375]A Grader (Caterpillar DG07) named "EC520-P_212" is currently at [12.982995714438859, 80.24393531044313, 123.43499755859375]A Excavator (Caterpillar DG07) named "EC520-P_225" is currently at [12.982995714438859, 80.24393531044313, 123.43499755859375]Conclusion
Section titled “Conclusion”We’ve now successfully traversed the Equipment API to get the position of our equipment. Note that after making these requests, we now know both:
- The data we were interested in
- The URLs that point to specific parts of the data, such as the current position(s).
As a long-lived client, we can now “bookmark” these URLs and reuse them to poll exactly the data we need without traversing the full API again. Note that the URLs may change infrequently, so the application needs to be able to rediscover the URLs in that case.
We could also improve this code by running some of the requests asynchronously - In the example, we iterated through everything sequentially. Some requests depend on previous responses and must be done in series. As a general rule, each time the traversal branches is a point where we could parallelize the requests.