Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions datacommons_client/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Data Commons Python API

This is a Python library for accessing data in the Data Commons Graph.
This is a Python library for accessing data in the Data Commons Graph via the V2 REST API (`node`, `observation`, `resolve`) and the SDMX 3.0 REST API (`data`, `availability`).

To get started, install this package from pip.

Expand All @@ -15,10 +15,61 @@ with the optional Pandas dependency.
pip install "datacommons-client[Pandas]"
```

Once the package is installed, import `datacommons_client`.
Once the package is installed, import `datacommons_client` and initialize `DataCommonsClient`:

```python
import datacommons_client as dc
from datacommons_client import DataCommonsClient

client = DataCommonsClient(api_key="YOUR_API_KEY")

# V2 Observation query
observations = client.observation.fetch(
variable_dcids="Count_Person",
entity_dcids=["country/USA"],
)

# SDMX 3.0 Data query (returns SDMX-CSV string, or use fetch_data_as_dataframe for a Pandas DataFrame)
csv_data = client.sdmx.fetch_data(
variable="Count_Person",
constraints={"observationAbout": "country/USA"},
)
df = client.sdmx.fetch_data_as_dataframe(
variable="Count_Person",
constraints={"observationAbout": "country/USA"},
)

# SDMX 3.0 Availability query (returns parsed SDMX-JSON, or use fetch_available_values for {component_id: [values]})
availability = client.sdmx.fetch_availability(
component_id="provenance",
variable="Count_Person",
)
available_values = client.sdmx.fetch_available_values(
component_id="provenance",
variable="Count_Person",
)
```

## Connecting to a Private Data Commons Platform Instance

To query a private [Data Commons Platform](https://github.com/datacommonsorg/datacommons) instance protected by IAM (such as an authenticated Cloud Run service), pass the instance URL along with your `Authorization` header and set `validate_instance=False`:

```python
from datacommons_client import DataCommonsClient

client = DataCommonsClient(
url="https://your-dcp-service-url.a.run.app/core/api/v2",
headers={"Authorization": f"Bearer {id_token}"},
validate_instance=False,
)

# Query V2 or SDMX 3.0 endpoints on the private instance
node_data = client.node.fetch(node_dcids="FinancialTrade", expression="->name")
sdmx_csv = client.sdmx.fetch_data(
variable="FinancialTrade",
constraints={"sourceCountry": "country/FRA"},
)
```

For more detail on getting started with the API, please visit <https://docs.datacommons.org/api/python/v2/>.


14 changes: 14 additions & 0 deletions datacommons_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,27 @@
from datacommons_client.endpoints.node import NodeEndpoint
from datacommons_client.endpoints.observation import ObservationEndpoint
from datacommons_client.endpoints.resolve import ResolveEndpoint
from datacommons_client.endpoints.sdmx import ApiLayout
from datacommons_client.endpoints.sdmx import build_query_params
from datacommons_client.endpoints.sdmx import parse_filters
from datacommons_client.endpoints.sdmx import SdmxClient
from datacommons_client.endpoints.sdmx import SdmxEndpoint
from datacommons_client.utils.context import use_api_key
from datacommons_client.utils.error_handling import SdmxAPIError
from datacommons_client.utils.error_handling import SdmxClientError

__all__ = [
"DataCommonsClient",
"API",
"NodeEndpoint",
"ObservationEndpoint",
"ResolveEndpoint",
"SdmxEndpoint",
"SdmxClient",
"ApiLayout",
"SdmxClientError",
"SdmxAPIError",
"parse_filters",
"build_query_params",
"use_api_key",
]
18 changes: 15 additions & 3 deletions datacommons_client/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from collections.abc import Mapping
from typing import Literal, Optional

from datacommons_client.endpoints.base import API
from datacommons_client.endpoints.node import NodeEndpoint
from datacommons_client.endpoints.observation import ObservationEndpoint
from datacommons_client.endpoints.resolve import ResolveEndpoint
from datacommons_client.endpoints.sdmx import SdmxEndpoint
from datacommons_client.models.observation import ObservationDate
from datacommons_client.utils.dataframes import add_entity_names_to_observations_dataframe
from datacommons_client.utils.dataframes import add_property_constraints_to_observations_dataframe
Expand All @@ -20,7 +22,8 @@ class DataCommonsClient:
"""
A client for interacting with the Data Commons API.

This class provides convenient access to the V2 Data Commons API endpoints.
This class provides convenient access to the V2 Data Commons API endpoints
and the SDMX 3.0 Data and Availability APIs.

Attributes:
api (API): An instance of the API class that handles requests.
Expand All @@ -29,6 +32,7 @@ class DataCommonsClient:
observation (ObservationEndpoint): Handles observation-related queries, allowing retrieval of
statistical observations associated with entities, variables, and dates (e.g., GDP of California in 2010).
resolve (ResolveEndpoint): Manages resolution queries to find different DCIDs for entities.
sdmx (SdmxEndpoint): Queries the SDMX 3.0 Data and Availability REST APIs.

"""

Expand All @@ -37,7 +41,9 @@ def __init__(self,
*,
dc_instance: Optional[str] = "datacommons.org",
url: Optional[str] = None,
surface_header_value: Optional[str] = None):
surface_header_value: Optional[str] = None,
headers: Optional[Mapping[str, str]] = None,
validate_instance: bool = True):
"""
Initializes the DataCommonsClient.

Expand All @@ -46,6 +52,9 @@ def __init__(self,
custom DC instances do not currently require an API key.
dc_instance (Optional[str]): The Data Commons instance to use. Defaults to "datacommons.org".
url (Optional[str]): A custom, fully resolved URL for the Data Commons API. Defaults to None.
surface_header_value (Optional[str]): Optional Data Commons surface header identifier.
headers (Optional[Mapping[str, str]]): Optional additional HTTP headers (e.g. Authorization bearer token).
validate_instance (bool): Whether to validate the target instance URL during initialization. Defaults to True.
"""
# If a fully resolved URL is provided, and the default dc_instance is used,
# ignore that default value
Expand All @@ -56,12 +65,15 @@ def __init__(self,
self.api = API(api_key=api_key,
dc_instance=dc_instance,
url=url,
surface_header_value=surface_header_value)
surface_header_value=surface_header_value,
headers=headers,
validate_instance=validate_instance)

# Create instances of the endpoints
self.node = NodeEndpoint(api=self.api)
self.observation = ObservationEndpoint(api=self.api)
self.resolve = ResolveEndpoint(api=self.api)
self.sdmx = SdmxEndpoint(api=self.api)

def _find_filter_facet_ids(
self,
Expand Down
57 changes: 47 additions & 10 deletions datacommons_client/endpoints/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from collections.abc import Mapping
import re
from typing import Any, Dict, Optional

from datacommons_client.utils.context import _API_KEY_CONTEXT_VAR
from datacommons_client.utils.request_handling import check_instance_is_valid
from datacommons_client.utils.request_handling import CUSTOM_DC_V2
from datacommons_client.utils.request_handling import post_request
from datacommons_client.utils.request_handling import resolve_instance_url

Expand All @@ -21,6 +23,9 @@ def __init__(
dc_instance: Optional[str] = None,
url: Optional[str] = None,
surface_header_value: Optional[str] = None,
*,
headers: Optional[Mapping[str, str]] = None,
validate_instance: bool = True,
):
"""
Initializes the API instance.
Expand All @@ -33,7 +38,9 @@ def __init__(
of the API is required (for local development, for example). If provided, dc_instance`
should not be provided.
surface_header_value: indicates which DC surface (MCP server, etc.) makes a call to the python library.
If the call originated internally, this is null and we pass in "clientlib-python" as the surface header
If the call originated internally, this is null and we pass in "clientlib-python" as the surface header
headers: Optional additional HTTP headers (e.g. Authorization bearer tokens) to include in requests.
validate_instance: Whether to probe the instance URL during initialization. Defaults to True.

Raises:
ValueError: If both `dc_instance` and `url` are provided.
Expand All @@ -45,14 +52,33 @@ def __init__(
dc_instance = "datacommons.org"

if url is not None:
# Use the given URL directly (strip trailing slash)
self.base_url = check_instance_is_valid(url.rstrip("/"), api_key=api_key)
clean_url = url.rstrip("/")
if not validate_instance:
self.base_url = clean_url
elif headers:
self.base_url = check_instance_is_valid(clean_url,
api_key=api_key,
headers=dict(headers))
else:
self.base_url = check_instance_is_valid(clean_url, api_key=api_key)
else:
# Resolve from dc_instance
self.base_url = resolve_instance_url(dc_instance)
clean_dc = (dc_instance.replace("https://", "").replace("http://",
"").rstrip("/"))
if not validate_instance:
if clean_dc == "datacommons.org":
self.base_url = resolve_instance_url("datacommons.org")
else:
self.base_url = f"https://{clean_dc}{CUSTOM_DC_V2}"
elif headers:
self.base_url = resolve_instance_url(dc_instance,
api_key=api_key,
headers=dict(headers))
else:
self.base_url = resolve_instance_url(dc_instance, api_key=api_key)

self.headers = self.build_headers(surface_header_value=surface_header_value,
api_key=api_key)
api_key=api_key,
custom_headers=headers)

def __repr__(self) -> str:
"""Returns a readable representation of the API object.
Expand All @@ -62,7 +88,9 @@ def __repr__(self) -> str:
Returns:
str: A string representation of the API object.
"""
has_auth = " (Authenticated)" if "X-API-Key" in self.headers else ""
has_auth = (" (Authenticated)" if any(
k.lower() in ("x-api-key", "authorization") for k in self.headers) else
"")
return f"<API at {self.base_url}{has_auth}>"

def post(self,
Expand Down Expand Up @@ -108,15 +136,21 @@ def post(self,
all_pages=all_pages,
next_token=next_token)

def build_headers(self,
surface_header_value: Optional[str],
api_key: Optional[str] = None) -> dict[str, str]:
def build_headers(
self,
surface_header_value: Optional[str],
api_key: Optional[str] = None,
custom_headers: Optional[Mapping[str, str]] = None,
) -> dict[str, str]:
"""Build request headers for API requests.

Includes JSON content type. If an API key is provided, add it as `X-API-Key`.

Args:
self: the API, which includes API key and surface header if available
surface_header_value: Optional surface header identifier.
api_key: Optional API key for X-API-Key header.
custom_headers: Optional custom headers to merge into the request headers.

Returns:
A dictionary of headers for the request.
Expand All @@ -131,6 +165,9 @@ def build_headers(self,
if surface_header_value:
headers["x-surface"] = surface_header_value

if custom_headers:
headers.update(custom_headers)

return headers


Expand Down
Loading
Loading