Skip to content

Commit b3178e6

Browse files
authored
Add endpoint-specific Endpoint classes (#213)
Introduced several changes to the package: - Introduces the specific Endpoint classes and tests - Removes the Sparql endpoint - Adds support for `next_token` and to fetch `all_pages` (as default) - Improves docstrings and makes other smaller tweaks
1 parent 8fb71b3 commit b3178e6

File tree

16 files changed

+1295
-314
lines changed

16 files changed

+1295
-314
lines changed

datacommons_client/endpoints/base.py

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
class API:
1010
"""Represents a configured API interface to the Data Commons API.
1111
12-
This class handles environment setup, resolving the base URL, building headers,
13-
or optionally using a fully qualified URL directly. It can be used standalone
14-
to interact with the API or in combination with Endpoint classes.
15-
"""
12+
This class handles environment setup, resolving the base URL, building headers,
13+
or optionally using a fully qualified URL directly. It can be used standalone
14+
to interact with the API or in combination with Endpoint classes.
15+
"""
1616

1717
def __init__(
1818
self,
@@ -21,19 +21,19 @@ def __init__(
2121
url: Optional[str] = None,
2222
):
2323
"""
24-
Initializes the API instance.
25-
26-
Args:
27-
api_key: The API key for authentication. Defaults to None.
28-
dc_instance: The Data Commons instance domain. Ignored if `url` is provided.
29-
Defaults to 'datacommons.org' if both `url` and `dc_instance` are None.
30-
url: A fully qualified URL for the base API. This may be useful if more granular control
31-
of the API is required (for local development, for example). If provided, dc_instance`
32-
should not be provided.
33-
34-
Raises:
35-
ValueError: If both `dc_instance` and `url` are provided.
36-
"""
24+
Initializes the API instance.
25+
26+
Args:
27+
api_key: The API key for authentication. Defaults to None.
28+
dc_instance: The Data Commons instance domain. Ignored if `url` is provided.
29+
Defaults to 'datacommons.org' if both `url` and `dc_instance` are None.
30+
url: A fully qualified URL for the base API. This may be useful if more granular control
31+
of the API is required (for local development, for example). If provided, dc_instance`
32+
should not be provided.
33+
34+
Raises:
35+
ValueError: If both `dc_instance` and `url` are provided.
36+
"""
3737
if dc_instance and url:
3838
raise ValueError("Cannot provide both `dc_instance` and `url`.")
3939

@@ -52,81 +52,105 @@ def __init__(
5252
def __repr__(self) -> str:
5353
"""Returns a readable representation of the API object.
5454
55-
Indicates the base URL and if it's authenticated.
55+
Indicates the base URL and if it's authenticated.
5656
57-
Returns:
58-
str: A string representation of the API object.
59-
"""
57+
Returns:
58+
str: A string representation of the API object.
59+
"""
6060
has_auth = " (Authenticated)" if "X-API-Key" in self.headers else ""
6161
return f"<API at {self.base_url}{has_auth}>"
6262

63-
def post(self,
64-
payload: dict[str, Any],
65-
endpoint: Optional[str] = None) -> Dict[str, Any]:
63+
def post(
64+
self,
65+
payload: dict[str, Any],
66+
endpoint: Optional[str] = None,
67+
*,
68+
all_pages: bool = True,
69+
next_token: Optional[str] = None,
70+
) -> Dict[str, Any]:
6671
"""Makes a POST request using the configured API environment.
6772
68-
If `endpoint` is provided, it will be appended to the base_url. Otherwise,
69-
it will just POST to the base URL.
73+
If `endpoint` is provided, it will be appended to the base_url. Otherwise,
74+
it will just POST to the base URL.
7075
71-
Args:
72-
payload: The JSON payload for the POST request.
73-
endpoint: An optional endpoint path to append to the base URL.
76+
Args:
77+
payload: The JSON payload for the POST request.
78+
endpoint: An optional endpoint path to append to the base URL.
79+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
80+
Defaults to True. Set to False to only fetch the first page. In that case, a
81+
`next_token` key in the response will indicate if more pages are available.
82+
That token can be used to fetch the next page.
7483
75-
Returns:
76-
A dictionary containing the merged response data.
84+
Returns:
85+
A dictionary containing the merged response data.
7786
78-
Raises:
79-
ValueError: If the payload is not a valid dictionary.
80-
"""
87+
Raises:
88+
ValueError: If the payload is not a valid dictionary.
89+
"""
8190
if not isinstance(payload, dict):
8291
raise ValueError("Payload must be a dictionary.")
8392

8493
url = (self.base_url if endpoint is None else f"{self.base_url}/{endpoint}")
85-
return post_request(url=url, payload=payload, headers=self.headers)
94+
return post_request(url=url,
95+
payload=payload,
96+
headers=self.headers,
97+
all_pages=all_pages,
98+
next_token=next_token)
8699

87100

88101
class Endpoint:
89102
"""Represents a specific endpoint within the Data Commons API.
90103
91-
This class leverages an API instance to make requests. It does not
92-
handle instance resolution or headers directly; that is delegated to the API instance.
104+
This class leverages an API instance to make requests. It does not
105+
handle instance resolution or headers directly; that is delegated to the API instance.
93106
94-
Attributes:
95-
endpoint (str): The endpoint path (e.g., 'node').
96-
api (API): The API instance providing configuration and the `post` method.
97-
"""
107+
Attributes:
108+
endpoint (str): The endpoint path (e.g., 'node').
109+
api (API): The API instance providing configuration and the `post` method.
110+
"""
98111

99112
def __init__(self, endpoint: str, api: API):
100113
"""
101-
Initializes the Endpoint instance.
114+
Initializes the Endpoint instance.
102115
103-
Args:
104-
endpoint: The endpoint path (e.g., 'node').
105-
api: An API instance that provides the environment configuration.
106-
"""
116+
Args:
117+
endpoint: The endpoint path (e.g., 'node').
118+
api: An API instance that provides the environment configuration.
119+
"""
107120
self.endpoint = endpoint
108121
self.api = api
109122

110123
def __repr__(self) -> str:
111124
"""Returns a readable representation of the Endpoint object.
112125
113-
Shows the endpoint and underlying API configuration.
126+
Shows the endpoint and underlying API configuration.
114127
115-
Returns:
116-
str: A string representation of the Endpoint object.
117-
"""
128+
Returns:
129+
str: A string representation of the Endpoint object.
130+
"""
118131
return f"<{self.endpoint.title()} Endpoint using {repr(self.api)}>"
119132

120-
def post(self, payload: dict[str, Any]) -> Dict[str, Any]:
133+
def post(self,
134+
payload: dict[str, Any],
135+
all_pages: bool = True,
136+
next_token: Optional[str] = None) -> Dict[str, Any]:
121137
"""Makes a POST request to the specified endpoint using the API instance.
122138
123-
Args:
124-
payload: The JSON payload for the POST request.
139+
Args:
140+
payload: The JSON payload for the POST request.
141+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
142+
Defaults to True. Set to False to only fetch the first page. In that case, a
143+
`next_token` key in the response will indicate if more pages are available.
144+
That token can be used to fetch the next page.
145+
next_token: Optionally, the token to fetch the next page of results. Defaults to None.
125146
126-
Returns:
127-
A dictionary with the merged API response data.
147+
Returns:
148+
A dictionary with the merged API response data.
128149
129-
Raises:
130-
ValueError: If the payload is not a valid dictionary.
131-
"""
132-
return self.api.post(payload=payload, endpoint=self.endpoint)
150+
Raises:
151+
ValueError: If the payload is not a valid dictionary.
152+
"""
153+
return self.api.post(payload=payload,
154+
endpoint=self.endpoint,
155+
all_pages=all_pages,
156+
next_token=next_token)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from typing import Optional
2+
3+
from datacommons_client.endpoints.base import API
4+
from datacommons_client.endpoints.base import Endpoint
5+
from datacommons_client.endpoints.payloads import NodeRequestPayload
6+
from datacommons_client.endpoints.payloads import \
7+
normalize_properties_to_string
8+
from datacommons_client.endpoints.response import NodeResponse
9+
10+
11+
class NodeEndpoint(Endpoint):
12+
"""Initializes the NodeEndpoint with a given API configuration.
13+
14+
Args:
15+
api (API): The API instance providing the environment configuration
16+
(base URL, headers, authentication) to be used for requests.
17+
"""
18+
19+
def __init__(self, api: API):
20+
"""Initializes the NodeEndpoint with a given API configuration."""
21+
super().__init__(endpoint="node", api=api)
22+
23+
def fetch(
24+
self,
25+
node_dcids: str | list[str],
26+
expression: str | list[str],
27+
*,
28+
all_pages: bool = True,
29+
next_token: Optional[str] = None,
30+
) -> NodeResponse:
31+
"""Fetches properties or arcs for given nodes and properties.
32+
33+
Args:
34+
node_dcids (str | List[str]): The DCID(s) of the nodes to query.
35+
expression (str | List[str]): The property or relation expression(s) to query.
36+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
37+
Defaults to True. Set to False to only fetch the first page. In that case, a
38+
`next_token` key in the response will indicate if more pages are available.
39+
That token can be used to fetch the next page.
40+
next_token: Optionally, the token to fetch the next page of results. Defaults to None.
41+
42+
Returns:
43+
NodeResponse: The response object containing the queried data.
44+
45+
Example:
46+
```python
47+
response = node_endpoint.fetch(
48+
node_dcids=["geoId/06"],
49+
expression="<-"
50+
)
51+
print(response.data)
52+
```
53+
"""
54+
55+
# Create the payload
56+
payload = NodeRequestPayload(node_dcids=node_dcids,
57+
expression=expression).to_dict
58+
59+
# Make the request and return the response.
60+
return NodeResponse.from_json(
61+
self.post(payload, all_pages=all_pages, next_token=next_token))
62+
63+
def fetch_property_labels(
64+
self,
65+
node_dcids: str | list[str],
66+
out: bool = True,
67+
*,
68+
all_pages: bool = True,
69+
next_token: Optional[str] = None,
70+
) -> NodeResponse:
71+
"""Fetches all property labels for the given nodes.
72+
73+
Args:
74+
node_dcids (str | list[str]): The DCID(s) of the nodes to query.
75+
out (bool): Whether to fetch outgoing properties (`->`). Defaults to True.
76+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
77+
Defaults to True. Set to False to only fetch the first page. In that case, a
78+
`next_token` key in the response will indicate if more pages are available.
79+
That token can be used to fetch the next page.
80+
next_token: Optionally, the token to fetch the next page of results. Defaults to None.
81+
82+
Returns:
83+
NodeResponse: The response object containing the property labels.
84+
85+
Example:
86+
```python
87+
response = node_endpoint.fetch_properties(node_dcids="geoId/06")
88+
print(response.data)
89+
```
90+
"""
91+
# Determine the direction of the properties.
92+
expression = "->" if out else "<-"
93+
94+
# Make the request and return the response.
95+
return self.fetch(node_dcids=node_dcids,
96+
expression=expression,
97+
all_pages=all_pages,
98+
next_token=next_token)
99+
100+
def fetch_property_values(
101+
self,
102+
node_dcids: str | list[str],
103+
properties: str | list[str],
104+
constraints: Optional[str] = None,
105+
out: bool = True,
106+
*,
107+
all_pages: bool = True,
108+
next_token: Optional[str] = None,
109+
) -> NodeResponse:
110+
"""Fetches the values of specific properties for given nodes.
111+
112+
Args:
113+
node_dcids (str | List[str]): The DCID(s) of the nodes to query.
114+
properties (str | List[str]): The property or relation expression(s) to query.
115+
constraints (Optional[str]): Additional constraints for the query. Defaults to None.
116+
out (bool): Whether to fetch outgoing properties. Defaults to True.
117+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
118+
Defaults to True. Set to False to only fetch the first page. In that case, a
119+
`next_token` key in the response will indicate if more pages are available.
120+
That token can be used to fetch the next page.
121+
next_token: Optionally, the token to fetch the next page of results. Defaults to None.
122+
123+
124+
Returns:
125+
NodeResponse: The response object containing the property values.
126+
127+
Example:
128+
```python
129+
response = node_endpoint.fetch_property_values(
130+
node_dcids=["geoId/06"],
131+
properties="name",
132+
out=True
133+
)
134+
print(response.data)
135+
```
136+
"""
137+
138+
# Normalize the input to a string (if it's a list), otherwise use the string as is.
139+
properties = normalize_properties_to_string(properties)
140+
141+
# Construct the expression based on the direction and constraints.
142+
direction = "->" if out else "<-"
143+
expression = f"{direction}{properties}"
144+
if constraints:
145+
expression += f"{{{constraints}}}"
146+
147+
return self.fetch(node_dcids=node_dcids,
148+
expression=expression,
149+
all_pages=all_pages,
150+
next_token=next_token)
151+
152+
def fetch_all_classes(
153+
self,
154+
*,
155+
all_pages: bool = True,
156+
next_token: Optional[str] = None,
157+
) -> NodeResponse:
158+
"""Fetches all Classes available in the Data Commons knowledge graph.
159+
160+
Args:
161+
all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
162+
Defaults to True. Set to False to only fetch the first page. In that case, a
163+
`next_token` key in the response will indicate if more pages are available.
164+
That token can be used to fetch the next page.
165+
next_token: Optionally, the token to fetch the next page of results. Defaults to None.
166+
167+
168+
Returns:
169+
NodeResponse: The response object containing all statistical variables.
170+
171+
Example:
172+
```python
173+
response = node_endpoint.fetch_all_classes()
174+
print(response.data)
175+
```
176+
"""
177+
178+
return self.fetch_property_values(node_dcids="Class",
179+
properties="typeOf",
180+
out=False,
181+
all_pages=all_pages,
182+
next_token=next_token)

0 commit comments

Comments
 (0)